diff --git a/.eslintrc.json b/.eslintrc.json index cd1a22c5cca..3a4306c330a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -16,11 +16,12 @@ }, "extends": [ "eslint:recommended", + "plugin:@angular-eslint/recommended", "plugin:@typescript-eslint/recommended", "plugin:import/recommended", "plugin:import/typescript", - "prettier", "plugin:rxjs/recommended", + "prettier", "plugin:storybook/recommended" ], "settings": { @@ -34,6 +35,19 @@ } }, "rules": { + "@angular-eslint/component-class-suffix": 0, + "@angular-eslint/contextual-lifecycle": 0, + "@angular-eslint/directive-class-suffix": 0, + "@angular-eslint/no-empty-lifecycle-method": 0, + "@angular-eslint/no-host-metadata-property": 0, + "@angular-eslint/no-input-rename": 0, + "@angular-eslint/no-inputs-metadata-property": 0, + "@angular-eslint/no-output-native": 0, + "@angular-eslint/no-output-on-prefix": 0, + "@angular-eslint/no-output-rename": 0, + "@angular-eslint/no-outputs-metadata-property": 0, + "@angular-eslint/use-lifecycle-interface": "error", + "@angular-eslint/use-pipe-transform-interface": 0, "@typescript-eslint/explicit-member-accessibility": [ "error", { "accessibility": "no-public" } @@ -86,6 +100,11 @@ "error", { "zones": [ + { + "target": ["libs/**/*"], + "from": ["apps/**/*"], + "message": "Libs should not import app-specific code." + }, { // avoid specific frameworks or large dependencies in common "target": "./libs/common/**/*", diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 48a5dce5bae..848d2bfa410 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -6,6 +6,7 @@ ## Secrets Manager team files ## bitwarden_license/bit-web/src/app/secrets-manager @bitwarden/team-secrets-manager-dev +apps/web/src/app/secrets-manager/ @bitwarden/team-secrets-manager-dev ## Auth team files ## apps/browser/src/auth @bitwarden/team-auth-dev @@ -18,6 +19,9 @@ apps/web/src/connectors @bitwarden/team-auth-dev bitwarden_license/bit-web/src/app/auth @bitwarden/team-auth-dev libs/angular/src/auth @bitwarden/team-auth-dev libs/common/src/auth @bitwarden/team-auth-dev +# biometrics +apps/desktop/src/services/native-messaging.service.ts @bitwarden/team-auth-dev +app/browser/src/background/nativeMessaging.background.ts @bitwarden/team-auth-dev ## Tools team files ## apps/browser/src/tools @bitwarden/team-tools-dev @@ -29,6 +33,7 @@ libs/common/src/models/export @bitwarden/team-tools-dev libs/common/src/tools @bitwarden/team-tools-dev libs/importer @bitwarden/team-tools-dev libs/tools @bitwarden/team-tools-dev +bitwarden_license/bit-web/src/app/tools @bitwarden/team-tools-dev ## Localization/Crowdin (Tools team) apps/browser/src/_locales @bitwarden/team-tools-dev @@ -92,16 +97,23 @@ libs/common/src/autofill @bitwarden/team-autofill-dev # DuckDuckGo integration apps/desktop/native-messaging-test-runner @bitwarden/team-autofill-dev apps/desktop/src/services/native-message-handler.service.ts @bitwarden/team-autofill-dev -apps/desktop/src/services/native-messaging.service.ts @bitwarden/team-autofill-dev ## Component Library ## -.storybook @bitwarden/team-component-library -libs/components @bitwarden/team-component-library -apps/web/src/app/layouts/header +.storybook @bitwarden/team-design-system +libs/components @bitwarden/team-design-system +apps/browser/src/platform/popup/layout @bitwarden/team-design-system +apps/web/src/app/layouts @bitwarden/team-design-system ## Desktop native module ## apps/desktop/desktop_native @bitwarden/team-platform-dev +## Key management team files ## +apps/desktop/src/key-management @bitwarden/team-key-management-dev +apps/web/src/key-management @bitwarden/team-key-management-dev +apps/browser/src/key-management @bitwarden/team-key-management-dev +apps/cli/src/key-management @bitwarden/team-key-management-dev +libs/common/src/key-management @bitwarden/team-key-management-dev + ## DevOps team files ## /.github/workflows @bitwarden/dept-devops diff --git a/.github/renovate.json b/.github/renovate.json index e202e026675..a1200912dc8 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -105,6 +105,26 @@ "commitMessagePrefix": "[deps] Billing:", "reviewers": ["team:team-billing-dev"] }, + { + "matchPackageNames": [ + "@types/argon2-browser", + "@types/chrome", + "@types/firefox-webext-browser", + "@types/jquery", + "@types/node", + "@types/node-forge", + "argon2", + "argon2-browser", + "big-integer", + "node-forge", + "rxjs", + "type-fest", + "typescript" + ], + "description": "Platform owned dependencies", + "commitMessagePrefix": "[deps] Platform:", + "reviewers": ["team:team-platform-dev"] + }, { "matchPackageNames": [ "@angular-devkit/build-angular", @@ -119,30 +139,6 @@ "@angular/platform", "@angular/compiler", "@angular/router", - "@types/argon2-browser", - "@types/chrome", - "@types/firefox-webext-browser", - "@types/jquery", - "@types/node", - "@types/node-forge", - "argon2", - "argon2-browser", - "big-integer", - "bootstrap", - "jquery", - "node-forge", - "popper.js", - "rxjs", - "type-fest", - "typescript", - "zone.js" - ], - "description": "Platform owned dependencies", - "commitMessagePrefix": "[deps] Platform:", - "reviewers": ["team:team-platform-dev"] - }, - { - "matchPackageNames": [ "@compodoc/compodoc", "@ng-select/ng-select", "@storybook/addon-a11y", @@ -153,17 +149,21 @@ "@storybook/angular", "@types/react", "autoprefixer", + "bootstrap", "chromatic", + "jquery", "ngx-toastr", + "popper.js", "react", "react-dom", "remark-gfm", "storybook", - "tailwindcss" + "tailwindcss", + "zone.js" ], "description": "Component library owned dependencies", - "commitMessagePrefix": "[deps] Platform (CL):", - "reviewers": ["team:team-component-library"] + "commitMessagePrefix": "[deps] Design System:", + "reviewers": ["team:team-design-system"] }, { "matchPackageNames": [ diff --git a/.github/whitelist-capital-letters.txt b/.github/whitelist-capital-letters.txt index 3155ef348f7..b09829f7f4c 100644 --- a/.github/whitelist-capital-letters.txt +++ b/.github/whitelist-capital-letters.txt @@ -18,6 +18,7 @@ ./libs/admin-console/README.md ./libs/auth/README.md ./libs/billing/README.md +./libs/common/src/tools/integration/README.md ./libs/platform/README.md ./libs/tools/README.md ./libs/tools/export/vault-export/README.md diff --git a/.github/workflows/auto-branch-updater.yml b/.github/workflows/auto-branch-updater.yml index 90376c99560..e2f181680d9 100644 --- a/.github/workflows/auto-branch-updater.yml +++ b/.github/workflows/auto-branch-updater.yml @@ -29,7 +29,7 @@ jobs: run: echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: 'eu-web-${{ steps.setup.outputs.branch }}' fetch-depth: 0 diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 14bc578bef1..610769859fe 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -43,7 +43,7 @@ jobs: node_version: ${{ steps.retrieve-node-version.outputs.node_version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Get Package Version id: gen_vars @@ -73,7 +73,7 @@ jobs: working-directory: apps/browser steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Testing locales - extName length run: | @@ -111,10 +111,10 @@ jobs: _NODE_VERSION: ${{ needs.setup.outputs.node_version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -173,56 +173,63 @@ jobs: working-directory: browser-source/apps/browser - name: Upload Opera artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: dist-opera-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/dist-opera.zip if-no-files-found: error - # - name: Upload Opera MV3 artifact - # uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 - # with: - # name: dist-opera-MV3-${{ env._BUILD_NUMBER }}.zip - # path: browser-source/apps/browser/dist/dist-opera-mv3.zip - # if-no-files-found: error + - name: Upload Opera MV3 artifact (DO NOT USE FOR PROD) + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + with: + name: DO-NOT-USE-FOR-PROD-dist-opera-MV3-${{ env._BUILD_NUMBER }}.zip + path: browser-source/apps/browser/dist/dist-opera-mv3.zip + if-no-files-found: error - name: Upload Chrome MV3 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: dist-chrome-MV3-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/dist-chrome-mv3.zip if-no-files-found: error - name: Upload Chrome MV3 Beta artifact (DO NOT USE FOR PROD) - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: DO-NOT-USE-FOR-PROD-dist-chrome-MV3-beta-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/dist-chrome-mv3-beta.zip if-no-files-found: error - name: Upload Firefox artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: dist-firefox-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/dist-firefox.zip if-no-files-found: error + - name: Upload Firefox MV3 artifact (DO NOT USE FOR PROD) + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + with: + name: DO-NOT-USE-FOR-PROD-dist-firefox-MV3-${{ env._BUILD_NUMBER }}.zip + path: browser-source/apps/browser/dist/dist-firefox-mv3.zip + if-no-files-found: error + - name: Upload Edge artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: dist-edge-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/dist/dist-edge.zip if-no-files-found: error - # - name: Upload Edge MV3 artifact - # uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 - # with: - # name: dist-edge-MV3-${{ env._BUILD_NUMBER }}.zip - # path: browser-source/apps/browser/dist/dist-edge-mv3.zip - # if-no-files-found: error + - name: Upload Edge MV3 artifact (DO NOT USE FOR PROD) + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + with: + name: DO-NOT-USE-FOR-PROD-dist-edge-MV3-${{ env._BUILD_NUMBER }}.zip + path: browser-source/apps/browser/dist/dist-edge-mv3.zip + if-no-files-found: error - name: Upload browser source - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: browser-source-${{ env._BUILD_NUMBER }}.zip path: browser-source.zip @@ -230,7 +237,7 @@ jobs: - name: Upload coverage artifact if: false - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: coverage-${{ env._BUILD_NUMBER }}.zip path: browser-source/apps/browser/coverage/coverage-${{ env._BUILD_NUMBER }}.zip @@ -247,10 +254,10 @@ jobs: _NODE_VERSION: ${{ needs.setup.outputs.node_version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -345,7 +352,7 @@ jobs: ls -la - name: Upload Safari artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: dist-safari-${{ env._BUILD_NUMBER }}.zip path: apps/browser/dist/dist-safari.zip @@ -360,7 +367,7 @@ jobs: - build-safari steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -375,7 +382,7 @@ jobs: secrets: "crowdin-api-token" - name: Upload Sources - uses: crowdin/github-action@c953b17499daa6be3e5afbf7a63616fb02d8b18d # v1.19.0 + uses: crowdin/github-action@30849777a3cba6ee9a09e24e195272b8287a0a5b # v1.20.4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} @@ -416,7 +423,7 @@ jobs: secrets: "devops-alerts-slack-webhook-url" - name: Notify Slack on failure - uses: act10ns/slack@ed1309ab9862e57e9e583e51c7889486b9a00b0f # v2.0.0 + uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0 if: failure() env: SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }} diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index ad2ac539715..76d86b45500 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -43,7 +43,7 @@ jobs: node_version: ${{ steps.retrieve-node-version.outputs.node_version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Get Package Version id: retrieve-package-version @@ -65,15 +65,15 @@ jobs: strategy: matrix: os: - [ - { base: "linux", distro: "ubuntu-22.04" }, - { base: "mac", distro: "macos-13" } - ] + [ + { base: "linux", distro: "ubuntu-22.04" }, + { base: "mac", distro: "macos-13" } + ] license_type: - [ - { build_prefix: "oss", artifact_prefix: "-oss", readable: "open source license" }, - { build_prefix: "bit", artifact_prefix: "", readable: "commercial license"} - ] + [ + { build_prefix: "oss", artifact_prefix: "-oss", readable: "open source license" }, + { build_prefix: "bit", artifact_prefix: "", readable: "commercial license" } + ] runs-on: ${{ matrix.os.distro }} needs: - setup @@ -84,7 +84,7 @@ jobs: _WIN_PKG_VERSION: 3.5 steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Unix Vars run: | @@ -93,7 +93,7 @@ jobs: awk '{print tolower($0)}')" >> $GITHUB_ENV - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -130,14 +130,14 @@ jobs: matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt - name: Upload unix zip asset - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip if-no-files-found: error - name: Upload unix checksum asset - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt @@ -148,10 +148,10 @@ jobs: strategy: matrix: license_type: - [ - { build_prefix: "oss", artifact_prefix: "-oss", readable: "open source license" }, - { build_prefix: "bit", artifact_prefix: "", readable: "commercial license"} - ] + [ + { build_prefix: "oss", artifact_prefix: "-oss", readable: "open source license" }, + { build_prefix: "bit", artifact_prefix: "", readable: "commercial license" } + ] runs-on: windows-2022 needs: - setup @@ -162,7 +162,7 @@ jobs: _WIN_PKG_VERSION: 3.5 steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup Windows builder run: | @@ -171,7 +171,7 @@ jobs: choco install nasm --no-progress - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -241,7 +241,7 @@ jobs: - name: Package Chocolatey shell: pwsh - if: ${{ matrix.license_type.build_prefix }} == 'bit' + if: ${{ matrix.license_type.build_prefix == 'bit' }} run: | Copy-Item -Path stores/chocolatey -Destination dist/chocolatey -Recurse Copy-Item dist/${{ matrix.license_type.build_prefix }}/windows/bw.exe -Destination dist/chocolatey/tools @@ -269,14 +269,14 @@ jobs: -t sha256 | Out-File -Encoding ASCII ./dist/bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${env:_PACKAGE_VERSION}.txt - name: Upload windows zip asset - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip if-no-files-found: error - name: Upload windows checksum asset - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt @@ -284,18 +284,21 @@ jobs: - name: Upload Chocolatey asset if: matrix.license_type.build_prefix == 'bit' - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg path: apps/cli/dist/chocolatey/bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg if-no-files-found: error + + - name: Zip NPM Build Artifact + run: Get-ChildItem -Path .\build | Compress-Archive -DestinationPath .\bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip - name: Upload NPM Build Directory asset if: matrix.license_type.build_prefix == 'bit' - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip - path: apps/cli/build + path: apps/cli/bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip if-no-files-found: error snap: @@ -309,7 +312,7 @@ jobs: _PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Print environment run: | @@ -319,7 +322,7 @@ jobs: echo "BW Package Version: $_PACKAGE_VERSION" - name: Get bw linux cli - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: bw-linux-${{ env._PACKAGE_VERSION }}.zip path: apps/cli/dist/snap @@ -332,7 +335,7 @@ jobs: ls -alth - name: Build snap - uses: snapcore/action-build@2096990827aa966f773676c8a53793c723b6b40f # v1.2.0 + uses: snapcore/action-build@3bdaa03e1ba6bf59a65f84a751d943d549a54e79 # v1.3.0 with: path: apps/cli/dist/snap @@ -361,14 +364,14 @@ jobs: run: sudo snap remove bw - name: Upload snap asset - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bw_${{ env._PACKAGE_VERSION }}_amd64.snap path: apps/cli/dist/snap/bw_${{ env._PACKAGE_VERSION }}_amd64.snap if-no-files-found: error - name: Upload snap checksum asset - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt path: apps/cli/dist/snap/bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt @@ -405,7 +408,7 @@ jobs: secrets: "devops-alerts-slack-webhook-url" - name: Notify Slack on failure - uses: act10ns/slack@ed1309ab9862e57e9e583e51c7889486b9a00b0f # v2.0.0 + uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0 if: failure() env: SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }} diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 336db208572..bef58e70b60 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -38,7 +38,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Verify run: | @@ -67,7 +67,7 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Get Package Version id: retrieve-version @@ -140,10 +140,10 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -169,7 +169,7 @@ jobs: working-directory: ./ - name: Cache Native Module - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: path: | @@ -193,42 +193,42 @@ jobs: run: npm run dist:lin - name: Upload .deb artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb if-no-files-found: error - name: Upload .rpm artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm if-no-files-found: error - name: Upload .freebsd artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd if-no-files-found: error - name: Upload .snap artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap path: apps/desktop/dist/bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap if-no-files-found: error - name: Upload .AppImage artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: ${{ needs.setup.outputs.release_channel }}-linux.yml path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-linux.yml @@ -249,10 +249,10 @@ jobs: NODE_OPTIONS: --max_old_space_size=4096 steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -298,7 +298,7 @@ jobs: working-directory: ./ - name: Cache Native Module - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: path: apps/desktop/desktop_native/napi/*.node @@ -351,91 +351,91 @@ jobs: -NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z - name: Upload portable exe artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe path: apps/desktop/dist/Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe if-no-files-found: error - name: Upload installer exe artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe path: apps/desktop/dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe if-no-files-found: error - name: Upload appx ia32 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx if-no-files-found: error - name: Upload store appx ia32 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx if-no-files-found: error - name: Upload NSIS ia32 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z if-no-files-found: error - name: Upload appx x64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx if-no-files-found: error - name: Upload store appx x64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx if-no-files-found: error - name: Upload NSIS x64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z if-no-files-found: error - name: Upload appx ARM64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx if-no-files-found: error - name: Upload store appx ARM64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx if-no-files-found: error - name: Upload NSIS ARM64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z if-no-files-found: error - name: Upload nupkg artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg path: apps/desktop/dist/chocolatey/bitwarden.${{ env._PACKAGE_VERSION }}.nupkg if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: ${{ needs.setup.outputs.release_channel }}.yml path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release_channel }}.yml @@ -455,10 +455,10 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -481,14 +481,14 @@ jobs: - name: Cache Build id: build-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Cache Safari id: safari-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -581,7 +581,7 @@ jobs: working-directory: ./ - name: Cache Native Module - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: path: apps/desktop/desktop_native/napi/*.node @@ -619,10 +619,10 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -645,14 +645,14 @@ jobs: - name: Get Build Cache id: build-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Setup Safari Cache id: safari-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -745,7 +745,7 @@ jobs: working-directory: ./ - name: Cache Native Module - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: path: apps/desktop/desktop_native/napi/*.node @@ -761,7 +761,7 @@ jobs: run: npm run build - name: Download Browser artifact - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: path: ${{ github.workspace }}/browser-build-artifacts @@ -776,36 +776,44 @@ 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 - name: Upload .zip artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip if-no-files-found: error - name: Upload .dmg artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg if-no-files-found: error - name: Upload .dmg blockmap artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: ${{ needs.setup.outputs.release_channel }}-mac.yml path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-mac.yml @@ -828,10 +836,10 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -854,14 +862,14 @@ jobs: - name: Get Build Cache id: build-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Setup Safari Cache id: safari-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -954,7 +962,7 @@ jobs: working-directory: ./ - name: Cache Native Module - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: path: apps/desktop/desktop_native/napi/*.node @@ -970,7 +978,7 @@ jobs: run: npm run build - name: Download Browser artifact - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: path: ${{ github.workspace }}/browser-build-artifacts @@ -985,30 +993,38 @@ 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 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg 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 @@ -1028,10 +1044,10 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -1049,14 +1065,14 @@ jobs: - name: Get Build Cache id: build-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Setup Safari Cache id: safari-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -1159,7 +1175,7 @@ jobs: working-directory: ./ - name: Cache Native Module - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 id: cache with: path: apps/desktop/desktop_native/napi/*.node @@ -1175,7 +1191,7 @@ jobs: run: npm run build - name: Download Browser artifact - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: path: ${{ github.workspace }}/browser-build-artifacts @@ -1190,11 +1206,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: Resign run: | @@ -1207,7 +1230,7 @@ jobs: zip -r Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip Bitwarden.app - name: Upload masdev artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip path: apps/desktop/dist/mas-dev-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip @@ -1225,7 +1248,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -1240,7 +1263,7 @@ jobs: secrets: "crowdin-api-token" - name: Upload Sources - uses: crowdin/github-action@c953b17499daa6be3e5afbf7a63616fb02d8b18d # v1.19.0 + uses: crowdin/github-action@30849777a3cba6ee9a09e24e195272b8287a0a5b # v1.20.4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} @@ -1286,7 +1309,7 @@ jobs: secrets: "devops-alerts-slack-webhook-url" - name: Notify Slack on failure - uses: act10ns/slack@ed1309ab9862e57e9e583e51c7889486b9a00b0f # v2.0.0 + uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0 if: failure() env: SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }} diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index 8576fb6760a..d875078757c 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -45,7 +45,7 @@ jobs: node_version: ${{ steps.retrieve-node-version.outputs.node_version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Get GitHub sha as version id: version @@ -91,10 +91,10 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -130,7 +130,7 @@ jobs: run: zip -r web-${{ env._VERSION }}-${{ matrix.name }}.zip build - name: Upload ${{ matrix.name }} artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: web-${{ env._VERSION }}-${{ matrix.name }}.zip path: apps/web/web-${{ env._VERSION }}-${{ matrix.name }}.zip @@ -157,7 +157,7 @@ jobs: _VERSION: ${{ needs.setup.outputs.version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Check Branch to Publish env: @@ -194,7 +194,7 @@ jobs: secrets: "github-pat-bitwarden-devops-bot-repo-scope" - name: Download ${{ matrix.artifact_name }} artifact - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: name: web-${{ env._VERSION }}-${{ matrix.artifact_name }}.zip path: apps/web @@ -234,7 +234,7 @@ jobs: run: echo "name=$_AZ_REGISTRY/${PROJECT_NAME}:${IMAGE_TAG}" >> $GITHUB_OUTPUT - name: Build Docker image - uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5.1.0 + uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0 with: context: apps/web file: apps/web/Dockerfile @@ -255,7 +255,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -270,7 +270,7 @@ jobs: secrets: "crowdin-api-token" - name: Upload Sources - uses: crowdin/github-action@c953b17499daa6be3e5afbf7a63616fb02d8b18d # v1.19.0 + uses: crowdin/github-action@30849777a3cba6ee9a09e24e195272b8287a0a5b # v1.20.4 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} @@ -345,7 +345,7 @@ jobs: secrets: "devops-alerts-slack-webhook-url" - name: Notify Slack on failure - uses: act10ns/slack@ed1309ab9862e57e9e583e51c7889486b9a00b0f # v2.0.0 + uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0 if: failure() env: SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }} diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index ab08d509b37..c8dd3e77838 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -3,23 +3,34 @@ 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 - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Check out repo + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 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) @@ -27,13 +38,13 @@ jobs: echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 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 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: "~/.npm" key: ${{ runner.os }}-npm-chromatic-${{ hashFiles('**/package-lock.json') }} @@ -41,12 +52,12 @@ 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 - name: Publish to Chromatic - uses: chromaui/action@c9067691aca4a28d6fbb40d9eea6e144369fbcae # v10.9.5 + uses: chromaui/action@b984808b772126a9f44b2b7737b131b68a2ede32 # v11.7.1 with: token: ${{ secrets.GITHUB_TOKEN }} projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} diff --git a/.github/workflows/crowdin-pull.yml b/.github/workflows/crowdin-pull.yml index b6c2e276461..527dedb5a86 100644 --- a/.github/workflows/crowdin-pull.yml +++ b/.github/workflows/crowdin-pull.yml @@ -23,7 +23,7 @@ jobs: crowdin_project_id: "308189" steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -59,4 +59,3 @@ jobs: working_directory: apps/${{ matrix.app_name }} gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }} gpg_passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }} - diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 5aa92c4dd8a..7551538b3a1 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -7,7 +7,7 @@ on: inputs: environment: description: 'Environment' - default: 'QA' + default: 'USQA' type: choice options: - USQA @@ -256,7 +256,7 @@ jobs: - setup - artifact-check runs-on: ubuntu-22.04 - if: ${{ always() && contains( inputs.environment , 'QA' ) }} + if: ${{ always() && ( contains( inputs.environment , 'QA' ) || contains( inputs.environment , 'DEV' ) ) }} outputs: channel_id: ${{ steps.slack-message.outputs.channel_id }} ts: ${{ steps.slack-message.outputs.ts }} @@ -407,7 +407,7 @@ jobs: notify: name: Notify Slack with result runs-on: ubuntu-22.04 - if: ${{ always() && contains( inputs.environment , 'QA' ) }} + if: ${{ always() && ( contains( inputs.environment , 'QA' ) || contains( inputs.environment , 'DEV' ) ) }} needs: - setup - notify-start diff --git a/.github/workflows/label-issue-pull-request.yml b/.github/workflows/label-issue-pull-request.yml deleted file mode 100644 index e52bba36d63..00000000000 --- a/.github/workflows/label-issue-pull-request.yml +++ /dev/null @@ -1,24 +0,0 @@ -# Runs creation of Pull Requests -# If the PR destination branch is main, add a needs-qa label unless created by renovate[bot] ---- -name: Label Issue Pull Request - -on: - pull_request: - types: - - opened # Check when PR is opened - paths-ignore: - - .github/workflows/** # We don't need QA on workflow changes - branches: - - 'main' # We only want to check when PRs target main - -jobs: - add-needs-qa-label: - runs-on: ubuntu-latest - if: ${{ github.actor != 'renovate[bot]' }} - steps: - - name: Add label to pull request - uses: andymckay/labeler@e6c4322d0397f3240f0e7e30a33b5c5df2d39e90 # 1.0.4 - if: ${{ !github.event.pull_request.head.repo.fork }} - with: - add-labels: "needs-qa" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 24338909f71..5fb71947846 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Lint filenames (no capital characters) run: | @@ -49,7 +49,7 @@ jobs: echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml new file mode 100644 index 00000000000..6581e260900 --- /dev/null +++ b/.github/workflows/publish-cli.yml @@ -0,0 +1,234 @@ +--- +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 }} + defaults: + run: + working-directory: . + steps: + - name: Branch check + if: ${{ inputs.publish_type != 'Dry Run' }} + run: | + if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc-cli" ]]; then + echo "===================================" + echo "[!] Can only publish from the 'rc' or 'hotfix-rc-cli' branches" + echo "===================================" + exit 1 + fi + + - name: Version output + id: version-output + run: | + if [[ "${{ inputs.version }}" == "latest" || "${{ 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 "version=$VERSION" >> $GITHUB_OUTPUT + else + echo "Release Version: ${{ inputs.version }}" + echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT + fi + + - name: Create GitHub deployment + if: ${{ 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: 'CLI - Production' + description: 'Deployment ${{ steps.version-output.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@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - 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/download/cli-v${{ env._PKG_VERSION }}/bw_${{ env._PKG_VERSION }}_amd64.snap + + - name: Publish Snap & logout + if: ${{ 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@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - 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 + run: New-Item -ItemType directory -Path ./dist + + - name: Download artifacts + run: Invoke-WebRequest -Uri "https://github.com/bitwarden/clients/releases/download/cli-v${{ env._PKG_VERSION }}/bitwarden-cli.${{ env._PKG_VERSION }}.nupkg" -OutFile bitwarden-cli.${{ env._PKG_VERSION }}.nupkg + working-directory: apps/cli/dist + + - name: Push to Chocolatey + if: ${{ inputs.publish_type != 'Dry Run' }} + run: choco push --source=https://push.chocolatey.org/ + working-directory: apps/cli/dist + + 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@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - 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 and set up artifact + run: | + mkdir -p build + wget https://github.com/bitwarden/clients/releases/download/cli-v${{ env._PKG_VERSION }}/bitwarden-cli-${{ env._PKG_VERSION }}-npm-build.zip + unzip bitwarden-cli-${{ env._PKG_VERSION }}-npm-build.zip -d build + + - 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: ${{ 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() && 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: ${{ inputs.publish_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: ${{ inputs.publish_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-desktop.yml b/.github/workflows/publish-desktop.yml new file mode 100644 index 00000000000..d12072c7e6d --- /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 desktop 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 + +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: Branch check + if: ${{ inputs.publish_type != 'Dry Run' }} + run: | + if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc-desktop" ]]; then + echo "===================================" + echo "[!] Can only publish from the 'rc' or 'hotfix-rc-desktop' branches" + echo "===================================" + exit 1 + fi + + - name: Check Publish Version + id: version + run: | + if [[ "${{ inputs.version }}" == "latest" || "${{ 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 "version=$VERSION" >> $GITHUB_OUTPUT + + echo "Tag name: $TAG_NAME" + echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT + else + echo "Release Version: ${{ inputs.version }}" + echo "version=${{ inputs.version }}" + + $TAG_NAME="desktop-v${{ inputs.version }}" + + echo "Tag name: $TAG_NAME" + echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT + 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: ${{ 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 + needs: setup + 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: Create artifacts directory + run: mkdir -p apps/desktop/artifacts + + - name: Download artifacts + env: + GH_TOKEN: ${{ github.token }} + working-directory: apps/desktop/artifacts + run: gh release download ${{ env._RELEASE_TAG }} -R bitwarden/clients + + - name: Set staged rollout percentage + env: + RELEASE_CHANNEL: ${{ needs.setup.outputs.release-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: ${{ 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: ${{ inputs.publish_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: ${{ inputs.publish_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 }} + + snap: + name: Deploy Snap + runs-on: ubuntu-22.04 + needs: setup + if: inputs.snap_publish + env: + _PKG_VERSION: ${{ needs.setup.outputs.release-version }} + _RELEASE_TAG: ${{ needs.setup.outputs.tag-name }} + steps: + - name: Checkout Repo + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - 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/download/${{ env._RELEASE_TAG }}/bitwarden_${{ env._PKG_VERSION }}_amd64.snap + + - name: Deploy to Snap Store + if: ${{ 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: inputs.choco_publish + env: + _PKG_VERSION: ${{ needs.setup.outputs.release-version }} + _RELEASE_TAG: ${{ needs.setup.outputs.tag-name }} + steps: + - name: Checkout Repo + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - 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 + 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 + run: New-Item -ItemType directory -Path ./dist + working-directory: apps/desktop + + - name: Download artifacts + working-directory: apps/desktop/dist + run: Invoke-WebRequest -Uri "https://github.com/bitwarden/clients/releases/download/${{ env._RELEASE_TAG }}/bitwarden.${{ env._PKG_VERSION }}.nupkg" -OutFile bitwarden.${{ env._PKG_VERSION }}.nupkg + + - name: Push to Chocolatey + if: ${{ inputs.publish_type != 'Dry Run' }} + 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() && 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: ${{ inputs.publish_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: ${{ inputs.publish_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..4409da93560 --- /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 Release' + type: choice + options: + - Initial Release + - 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@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - name: Branch check + if: ${{ inputs.publish_type != 'Dry Run' }} + run: | + if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc-web" ]]; then + echo "===================================" + echo "[!] Can only publish 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: ${{ 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: ${{ 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@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + ########## 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: ${{ 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 [[ "${{ 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 [[ "${{ 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 [[ "${{ 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: ${{ 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: ${{ 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..2811b23af9b 100644 --- a/.github/workflows/release-browser.yml +++ b/.github/workflows/release-browser.yml @@ -27,7 +27,7 @@ jobs: release-version: ${{ steps.version.outputs.version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Branch check if: ${{ github.event.inputs.release_type != 'Dry Run' }} @@ -56,7 +56,7 @@ jobs: needs: setup steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Testing locales - extName length run: | @@ -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..cd450b2cd79 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: @@ -43,10 +27,10 @@ jobs: release-version: ${{ steps.version.outputs.version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Branch check - if: ${{ github.event.inputs.release_type != 'Dry Run' }} + if: ${{ inputs.release_type != 'Dry Run' }} run: | if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc-cli" ]]; then echo "===================================" @@ -59,25 +43,19 @@ jobs: id: version uses: bitwarden/gh-actions/release-version-check@main with: - release-type: ${{ github.event.inputs.release_type }} + release-type: ${{ inputs.release_type }} project-type: ts file: apps/cli/package.json 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' }} + if: ${{ inputs.release_type != 'Dry Run' }} uses: bitwarden/gh-actions/download-artifacts@main with: workflow: build-cli.yml @@ -86,7 +64,7 @@ jobs: branch: ${{ github.ref_name }} - name: Dry Run - Download all artifacts - if: ${{ github.event.inputs.release_type == 'Dry Run' }} + if: ${{ inputs.release_type == 'Dry Run' }} uses: bitwarden/gh-actions/download-artifacts@main with: workflow: build-cli.yml @@ -95,10 +73,10 @@ jobs: branch: main - name: Create release - if: ${{ github.event.inputs.release_type != 'Dry Run' }} + if: ${{ inputs.release_type != 'Dry Run' }} uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 env: - PKG_VERSION: ${{ steps.version.outputs.version }} + PKG_VERSION: ${{ needs.setup.outputs.release-version }} with: artifacts: "apps/cli/bw-oss-windows-${{ env.PKG_VERSION }}.zip, apps/cli/bw-oss-windows-sha256-${{ env.PKG_VERSION }}.txt, @@ -114,196 +92,11 @@ jobs: apps/cli/bw-linux-sha256-${{ env.PKG_VERSION }}.txt, apps/cli/bitwarden-cli.${{ env.PKG_VERSION }}.nupkg, apps/cli/bw_${{ env.PKG_VERSION }}_amd64.snap, - apps/cli/bw-snap-sha256-${{ env.PKG_VERSION }}.txt" + apps/cli/bw-snap-sha256-${{ env.PKG_VERSION }}.txt, + apps/cli/bitwarden-cli-${{ env.PKG_VERSION }}-npm-build.zip" commit: ${{ github.sha }} tag: cli-v${{ env.PKG_VERSION }} name: CLI v${{ env.PKG_VERSION }} 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-beta.yml b/.github/workflows/release-desktop-beta.yml index 74db61563e1..5a6a3d52361 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -24,7 +24,7 @@ jobs: node_version: ${{ steps.retrieve-node-version.outputs.node_version }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Branch check run: | @@ -125,12 +125,12 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: ${{ needs.setup.outputs.branch-name }} - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -159,42 +159,42 @@ jobs: run: npm run dist:lin - name: Upload .deb artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb if-no-files-found: error - name: Upload .rpm artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm if-no-files-found: error - name: Upload .freebsd artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd if-no-files-found: error - name: Upload .snap artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap path: apps/desktop/dist/bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap if-no-files-found: error - name: Upload .AppImage artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: ${{ needs.setup.outputs.release-channel }}-linux.yml path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-linux.yml @@ -215,12 +215,12 @@ jobs: NODE_OPTIONS: --max_old_space_size=4096 steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: ${{ needs.setup.outputs.branch-name }} - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -300,91 +300,91 @@ jobs: -NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z - name: Upload portable exe artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe path: apps/desktop/dist/Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe if-no-files-found: error - name: Upload installer exe artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe path: apps/desktop/dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe if-no-files-found: error - name: Upload appx ia32 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx if-no-files-found: error - name: Upload store appx ia32 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx if-no-files-found: error - name: Upload NSIS ia32 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z if-no-files-found: error - name: Upload appx x64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx if-no-files-found: error - name: Upload store appx x64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx if-no-files-found: error - name: Upload NSIS x64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z if-no-files-found: error - name: Upload appx ARM64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx if-no-files-found: error - name: Upload store appx ARM64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx if-no-files-found: error - name: Upload NSIS ARM64 artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z if-no-files-found: error - name: Upload nupkg artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg path: apps/desktop/dist/chocolatey/bitwarden.${{ env._PACKAGE_VERSION }}.nupkg if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: ${{ needs.setup.outputs.release-channel }}.yml path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release-channel }}.yml @@ -404,12 +404,12 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: ${{ needs.setup.outputs.branch-name }} - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -427,14 +427,14 @@ jobs: - name: Cache Build id: build-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Cache Safari id: safari-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -538,12 +538,12 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: ${{ needs.setup.outputs.branch-name }} - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -561,14 +561,14 @@ jobs: - name: Get Build Cache id: build-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Setup Safari Cache id: safari-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -708,28 +708,28 @@ jobs: run: npm run pack:mac - name: Upload .zip artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip if-no-files-found: error - name: Upload .dmg artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg if-no-files-found: error - name: Upload .dmg blockmap artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap if-no-files-found: error - name: Upload auto-update artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: ${{ needs.setup.outputs.release-channel }}-mac.yml path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-mac.yml @@ -751,12 +751,12 @@ jobs: working-directory: apps/desktop steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: ${{ needs.setup.outputs.branch-name }} - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -774,14 +774,14 @@ jobs: - name: Get Build Cache id: build-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/desktop/build key: ${{ runner.os }}-${{ github.run_id }}-build - name: Setup Safari Cache id: safari-cache - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2 with: path: apps/browser/dist/Safari key: ${{ runner.os }}-${{ github.run_id }}-safari-extension @@ -916,7 +916,7 @@ jobs: APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} - name: Upload .pkg artifact - uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 with: name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg @@ -958,7 +958,7 @@ jobs: aws-electron-bucket-name" - name: Download all artifacts - uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 with: path: apps/desktop/artifacts @@ -1011,7 +1011,7 @@ jobs: - release steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup git config run: | diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index eb63a53f2ea..5b75460ef92 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: @@ -48,7 +27,7 @@ jobs: release-channel: ${{ steps.release-channel.outputs.channel }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Branch check if: ${{ github.event.inputs.release_type != 'Dry 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: @@ -167,7 +98,7 @@ jobs: - name: Create Release uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 - if: ${{ steps.release-channel.outputs.channel == 'latest' && github.event.inputs.release_type != 'Dry Run' && github.event.inputs.github_release == 'true' }} + if: ${{ steps.release-channel.outputs.channel == 'latest' && github.event.inputs.release_type != 'Dry Run' }} env: PKG_VERSION: ${{ steps.version.outputs.version }} RELEASE_CHANNEL: ${{ steps.release-channel.outputs.channel }} @@ -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..982e3867585 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 @@ -27,7 +24,7 @@ jobs: tag_version: ${{ steps.version.outputs.tag }} steps: - name: Checkout repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Branch check if: ${{ github.event.inputs.release_type != 'Dry Run' }} @@ -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/retrieve-current-desktop-rollout.yml b/.github/workflows/retrieve-current-desktop-rollout.yml new file mode 100644 index 00000000000..45a2bf5ce42 --- /dev/null +++ b/.github/workflows/retrieve-current-desktop-rollout.yml @@ -0,0 +1,42 @@ +--- +name: Retrieve Current Desktop Rollout + +on: + workflow_dispatch: + +defaults: + run: + shell: bash + +jobs: + rollout: + name: Retrieve Rollout Percentage + runs-on: ubuntu-22.04 + 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 channel update info files from S3 + 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 }} + run: aws s3 cp $AWS_S3_BUCKET_NAME/desktop/latest.yml . --quiet + + - name: Get current rollout percentage + run: | + CURRENT_PCT=$(sed -r -n "s/stagingPercentage:\s([0-9]+)/\1/p" latest.yml) + CURRENT_VERSION=$(sed -r -n "s/version:\s(.*)/\1/p" latest.yml) + echo "Desktop ${CURRENT_VERSION} rollout percentage is ${CURRENT_PCT}%" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index 6e010d1b7ed..076bfb46e80 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -1,3 +1,4 @@ +--- name: Scan on: @@ -26,12 +27,12 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: ${{ github.event.pull_request.head.sha }} - name: Scan with Checkmarx - uses: checkmarx/ast-github-action@749fec53e0db0f6404a97e2e0807c3e80e3583a7 #2.0.23 + uses: checkmarx/ast-github-action@1fe318de2993222574e6249750ba9000a4e2a6cd # 2.0.33 env: INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}" with: @@ -46,7 +47,7 @@ jobs: --output-path . ${{ env.INCREMENTAL }} - name: Upload Checkmarx results to GitHub - uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9 + uses: github/codeql-action/upload-sarif@4dd16135b69a43b6c8efb853346f8437d92d3c93 # v3.26.6 with: sarif_file: cx_result.sarif @@ -60,13 +61,13 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} - name: Scan with SonarCloud - uses: sonarsource/sonarcloud-github-action@49e6cd3b187936a73b8280d59ffd9da69df63ec9 # v2.1.1 + uses: sonarsource/sonarcloud-github-action@eb211723266fe8e83102bac7361f0a05c3ac1d1b # v3.0.0 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -74,6 +75,7 @@ jobs: args: > -Dsonar.organization=${{ github.repository_owner }} -Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }} - -Dsonar.test.inclusions=**/*.spec.ts -Dsonar.tests=. -Dsonar.sources=. + -Dsonar.test.inclusions=**/*.spec.ts + -Dsonar.exclusions=**/*.spec.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 16238f15308..8d4067c1167 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,3 +1,4 @@ +--- name: Testing on: @@ -11,17 +12,36 @@ 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 pull-requests: write - + steps: - name: Check out repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Get Node Version id: retrieve-node-version @@ -31,7 +51,7 @@ jobs: echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT - name: Set up Node - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: cache: 'npm' cache-dependency-path: '**/package-lock.json' @@ -57,26 +77,23 @@ 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 coverage to codecov.io + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 + if: ${{ needs.check-test-secrets.outputs.available == 'true' }} + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - name: Upload to codecov.io - uses: codecov/codecov-action@54bcd8715eee62d40e33596ef5e8f0f48dbbccab # v4.1.0 - if: steps.check-codecov-secret.outputs.available == 'true' + - name: Upload results to codecov.io + uses: codecov/test-results-action@1b5b448b98e58ba90d1a1a1d9fcb72ca2263be46 # v1.0.0 + if: ${{ needs.check-test-secrets.outputs.available == 'true' }} env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} @@ -104,7 +121,7 @@ jobs: sudo apt-get install -y gnome-keyring dbus-x11 - name: Check out repo - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Build working-directory: ./apps/desktop/desktop_native @@ -121,7 +138,12 @@ jobs: eval "$(printf '\n' | /usr/bin/gnome-keyring-daemon --start)" cargo test -- --test-threads=1 - - name: Test Windows / macOS - if: ${{ matrix.os!='ubuntu-latest' }} + - name: Test macOS + if: ${{ matrix.os=='macos-latest' }} working-directory: ./apps/desktop/desktop_native run: cargo test -- --test-threads=1 + + - name: Test Windows + if: ${{ matrix.os=='windows-latest'}} + working-directory: ./apps/desktop/desktop_native/core + run: cargo test -- --test-threads=1 diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index 4bf502da21c..fc30996e850 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -58,7 +58,7 @@ jobs: fi - name: Checkout Branch - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: main @@ -526,7 +526,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Checkout Branch - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: main diff --git a/.storybook/main.ts b/.storybook/main.ts index 175ed339489..28bc2aa085c 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,3 +1,4 @@ +import { dirname, join } from "path"; import { StorybookConfig } from "@storybook/angular"; import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin"; import remarkGfm from "remark-gfm"; @@ -6,6 +7,8 @@ const config: StorybookConfig = { stories: [ "../libs/auth/src/**/*.mdx", "../libs/auth/src/**/*.stories.@(js|jsx|ts|tsx)", + "../libs/tools/send/send-ui/src/**/*.mdx", + "../libs/tools/send/send-ui/src/**/*.stories.@(js|jsx|ts|tsx)", "../libs/vault/src/**/*.mdx", "../libs/vault/src/**/*.stories.@(js|jsx|ts|tsx)", "../libs/components/src/**/*.mdx", @@ -18,11 +21,11 @@ const config: StorybookConfig = { "../bitwarden_license/bit-web/src/**/*.stories.@(js|jsx|ts|tsx)", ], addons: [ - "@storybook/addon-links", - "@storybook/addon-essentials", - "@storybook/addon-a11y", - "@storybook/addon-designs", - "@storybook/addon-interactions", + getAbsolutePath("@storybook/addon-links"), + getAbsolutePath("@storybook/addon-essentials"), + getAbsolutePath("@storybook/addon-a11y"), + getAbsolutePath("@storybook/addon-designs"), + getAbsolutePath("@storybook/addon-interactions"), { name: "@storybook/addon-docs", options: { @@ -35,7 +38,7 @@ const config: StorybookConfig = { }, ], framework: { - name: "@storybook/angular", + name: getAbsolutePath("@storybook/angular"), options: {}, }, core: { @@ -51,9 +54,12 @@ const config: StorybookConfig = { } return config; }, - docs: { - autodocs: true, - }, + docs: {}, }; export default config; + +// Recommended for mono-repositories +function getAbsolutePath(value: string): any { + return dirname(require.resolve(join(value, "package.json"))); +} diff --git a/.storybook/manager.js b/.storybook/manager.js index 89a69bf9421..409f93ec505 100644 --- a/.storybook/manager.js +++ b/.storybook/manager.js @@ -1,4 +1,4 @@ -import { addons } from "@storybook/addons"; +import { addons } from "@storybook/manager-api"; import { create } from "@storybook/theming/create"; const lightTheme = create({ diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index af20dcad74e..d1ba27e108d 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -92,7 +92,6 @@ const preview: Preview = { }, }, parameters: { - actions: { argTypesRegex: "^on[A-Z].*" }, controls: { matchers: { color: /(background|color)$/i, @@ -107,6 +106,7 @@ const preview: Preview = { }, docs: { source: { type: "dynamic", excludeDecorators: true } }, }, + tags: ["autodocs"], }; export default preview; diff --git a/apps/browser/config/base.json b/apps/browser/config/base.json index b6f24bf9ae3..5113cd7d1bf 100644 --- a/apps/browser/config/base.json +++ b/apps/browser/config/base.json @@ -2,7 +2,6 @@ "devFlags": {}, "flags": { "showPasswordless": true, - "enableCipherKeyEncryption": true, "accountSwitching": false } } diff --git a/apps/browser/config/development.json b/apps/browser/config/development.json index 950c5372d8f..cc28e15f38b 100644 --- a/apps/browser/config/development.json +++ b/apps/browser/config/development.json @@ -7,7 +7,6 @@ }, "flags": { "showPasswordless": true, - "enableCipherKeyEncryption": true, "accountSwitching": true } } diff --git a/apps/browser/config/production.json b/apps/browser/config/production.json index 64c6cb92a3b..a43eee1d5c9 100644 --- a/apps/browser/config/production.json +++ b/apps/browser/config/production.json @@ -1,6 +1,5 @@ { "flags": { - "enableCipherKeyEncryption": true, "accountSwitching": true } } diff --git a/apps/browser/gulpfile.js b/apps/browser/gulpfile.js index 3fe2c44dd19..89d944cdec8 100644 --- a/apps/browser/gulpfile.js +++ b/apps/browser/gulpfile.js @@ -8,6 +8,8 @@ const jeditor = require("gulp-json-editor"); const replace = require("gulp-replace"); const manifest = require("./src/manifest.json"); +const manifestVersion = parseInt(process.env.MANIFEST_VERSION || manifest.version); +const betaBuild = process.env.BETA_BUILD === "1"; const paths = { build: "./build/", @@ -48,7 +50,7 @@ function buildString() { if (process.env.MANIFEST_VERSION) { build = `-mv${process.env.MANIFEST_VERSION}`; } - if (process.env.BETA_BUILD === "1") { + if (betaBuild) { build += "-beta"; } if (process.env.BUILD_NUMBER && process.env.BUILD_NUMBER !== "") { @@ -76,12 +78,17 @@ async function dist(browserName, manifest) { function distFirefox() { return dist("firefox", (manifest) => { + if (manifestVersion === 3) { + const backgroundScript = manifest.background.service_worker; + delete manifest.background.service_worker; + manifest.background.scripts = [backgroundScript]; + } delete manifest.storage; delete manifest.sandbox; manifest.optional_permissions = manifest.optional_permissions.filter( (permission) => permission !== "privacy", ); - if (process.env.BETA_BUILD === "1") { + if (betaBuild) { manifest = applyBetaLabels(manifest); } return manifest; @@ -91,7 +98,16 @@ function distFirefox() { function distOpera() { return dist("opera", (manifest) => { delete manifest.applications; - if (process.env.BETA_BUILD === "1") { + + // Mv3 on Opera does seem to have sidebar support, however it is not working as expected. + // On install, the extension will crash the browser entirely if the sidebar_action key is set. + // We will remove the sidebar_action key for now until opera implements a fix. + if (manifestVersion === 3) { + delete manifest.sidebar_action; + delete manifest.commands._execute_sidebar_action; + } + + if (betaBuild) { manifest = applyBetaLabels(manifest); } return manifest; @@ -103,7 +119,7 @@ function distChrome() { delete manifest.applications; delete manifest.sidebar_action; delete manifest.commands._execute_sidebar_action; - if (process.env.BETA_BUILD === "1") { + if (betaBuild) { manifest = applyBetaLabels(manifest); } return manifest; @@ -115,7 +131,7 @@ function distEdge() { delete manifest.applications; delete manifest.sidebar_action; delete manifest.commands._execute_sidebar_action; - if (process.env.BETA_BUILD === "1") { + if (betaBuild) { manifest = applyBetaLabels(manifest); } return manifest; @@ -234,11 +250,16 @@ async function safariCopyBuild(source, dest) { gulpif( "manifest.json", jeditor((manifest) => { + if (manifestVersion === 3) { + const backgroundScript = manifest.background.service_worker; + delete manifest.background.service_worker; + manifest.background.scripts = [backgroundScript]; + } delete manifest.sidebar_action; delete manifest.commands._execute_sidebar_action; delete manifest.optional_permissions; manifest.permissions.push("nativeMessaging"); - if (process.env.BETA_BUILD === "1") { + if (betaBuild) { manifest = applyBetaLabels(manifest); } return manifest; diff --git a/apps/browser/package.json b/apps/browser/package.json index f7c577e7f7f..4d008b684cb 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,12 +1,15 @@ { "name": "@bitwarden/browser", - "version": "2024.7.0", + "version": "2024.9.1", "scripts": { "build": "cross-env MANIFEST_VERSION=3 webpack", "build:mv2": "webpack", - "build:watch": "cross-env MANIFEST_VERSION=3 webpack --watch", + "build:watch": "npm run build:watch:chrome", + "build:watch:chrome": "cross-env MANIFEST_VERSION=3 BROWSER=chrome webpack --watch", + "build:watch:firefox": "cross-env MANIFEST_VERSION=3 BROWSER=firefox webpack --watch", + "build:watch:safari": "cross-env MANIFEST_VERSION=3 BROWSER=safari webpack --watch", "build:watch:mv2": "webpack --watch", - "build:prod": "cross-env NODE_ENV=production webpack", + "build:prod": "cross-env NODE_ENV=production NODE_OPTIONS=\"--max-old-space-size=4096\" webpack", "build:prod:beta": "cross-env BETA_BUILD=1 NODE_ENV=production webpack", "build:prod:watch": "cross-env NODE_ENV=production webpack --watch", "dist": "npm run build:prod && gulp dist", @@ -17,7 +20,8 @@ "dist:chrome:beta": "cross-env MANIFEST_VERSION=3 npm run build:prod:beta && cross-env MANIFEST_VERSION=3 BETA_BUILD=1 gulp dist:chrome", "dist:firefox": "npm run build:prod && gulp dist:firefox", "dist:opera": "npm run build:prod && gulp dist:opera", - "dist:safari": "npm run build:prod && gulp dist:safari", + "dist:safari": "cross-env BROWSER=safari npm run build:prod && gulp dist:safari", + "dist:safari:mv3": "cross-env MANIFEST_VERSION=3 BROWSER=safari run build:prod && cross-env MANIFEST_VERSION=3 BROWSER=safari gulp dist:safari", "dist:safari:mas": "npm run build:prod && gulp dist:safari:mas", "dist:safari:masdev": "npm run build:prod && gulp dist:safari:masdev", "dist:safari:dmg": "npm run build:prod && gulp dist:safari:dmg", diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index fd3d9ac53af..e1fcf72c811 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "قم بالتسجيل أو إنشاء حساب جديد للوصول إلى خزنتك الآمنة." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "إنشاء حساب" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "تسجيل الدخول" - }, "enterpriseSingleSignOn": { "message": "تسجيل الدخول الأُحادي للمؤسسات – SSO" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "تلميح كلمة المرور الرئيسية (إختياري)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "علامة تبويب" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "نسخ رمز الأمان" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "التعبئة التلقائية" }, @@ -280,6 +301,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,16 +384,56 @@ "message": "الحد الأدنى لطول كلمة السر" }, "uppercase": { - "message": "أحرف كبيرة (من A إلى Z)" + "message": "أحرف كبيرة (من A إلى Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "أحرف كبيرة (من a إلى z)" + "message": "أحرف كبيرة (من a إلى z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "الأرقام (من 0 الى 9)" + "message": "الأرقام (من 0 الى 9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "الأحرف الخاصة (!@#$%^&*)" + "message": "الأحرف الخاصة (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "عدد الكلمات" @@ -376,7 +455,12 @@ "message": "الحد الأدنى من الأحرف الخاصة" }, "avoidAmbChar": { - "message": "تجنب الأحرف الغامضة" + "message": "تجنب الأحرف الغامضة", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "البحث في الخزنة" @@ -556,6 +640,18 @@ "security": { "message": "الأمان" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "لقد حدث خطأ ما" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "تم إنشاء حسابك الجديد! يمكنك الآن تسجيل الدخول." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "سجلتَ الدخول بنجاح" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "رمز التحقق مطلوب." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "رمز التحقق غير صالح" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "مسح رمز QR للمصادقة من صفحة الويب الحالية" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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": "نسخ مفتاح المصادقة (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "انتهت صلاحية جلسة تسجيل الدخول الخاصة بك." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "هل أنت متأكد من أنك تريد تسجيل الخروج؟" }, @@ -697,6 +829,10 @@ "newUri": { "message": "رابط جديد" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "تمت إضافة العنصر" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "اطلب إضافة تسجيل الدخول" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "اطلب إضافة عنصر إذا لم يُعثر عليه في خزنتك." }, "addLoginNotificationDescAlt": { "message": "اطلب إضافة عنصر إذا لم يتم العثور على عنصر في المخزن الخاص بك. ينطبق على جميع حسابات تسجيل الدخول." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "أظهر البطاقات في صفحة التبويبات" }, "showCardsCurrentTabDesc": { "message": "قائمة عناصر البطاقة في صفحة التبويب لسهولة التعبئة التلقائية." }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" + }, "showIdentitiesCurrentTab": { "message": "إظهار الهويات على صفحة التبويب" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "الكشف الافتراضي عن تطابق URI", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "اختر الطريقة الافتراضية التي يتم التعامل بها مع الكشف عن مطابقة URI لتسجيل الدخول عند تنفيذ إجراءات مثل التعبئة التلقائية." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 جيغابايت وحدة تخزين مشفرة لمرفقات الملفات." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "خيارات تسجيل الدخول بخطوتين المملوكة لجهات اخرى مثل YubiKey و Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "يمكنك شراء العضوية المتميزة على bitwarden.com على خزانة الويب. هل تريد زيارة الموقع الآن؟" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "أنت عضو مميز!" }, "premiumCurrentMemberThanks": { "message": "شكرا لك على دعم Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "الكل فقط بـ $PRICE$ /سنة!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "اكتمل التحديث" }, @@ -1179,10 +1342,19 @@ }, "showAutoFillMenuOnFormFields": { "message": "إظهار قائمة الملء التلقائي في حقول النموذج", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { - "message": "ينطبق على جميع الحسابات المسجل الدخول بها." + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { + "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { "message": "إيقاف تشغيل إعدادات مدير كلمات المرور الافتراضي في متصفحك لتجنب التضارب." @@ -1202,15 +1374,34 @@ "message": "عندما يتم اختيار ايقونة الملء التلقائي", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "ملء تلقائي عند تحميل الصفحة" }, "enableAutoFillOnPageLoadDesc": { "message": "إذا تم اكتشاف نموذج تسجيل الدخول، يتم التعبئة التلقائية عند تحميل صفحة الويب." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "مواقع المساومة أو غير الموثوق بها يمكن أن تستغل الملء التلقائي في تحميل الصفحة." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" + }, "learnMoreAboutAutofill": { "message": "تعرف على المزيد حول الملء التلقائي" }, @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "فتح المخزن في الشريط الجانبي" }, - "commandAutofillDesc": { - "message": "ملء تلقائي لآخر تسجيل دخول مستخدم للموقع الحالي" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "إنشاء واستنساخ كلمة مرور عشوائية جديدة إلى الحافظة" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "قيمة منطقية" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "مرتبط", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "سجل كلمة المرور" }, @@ -1533,6 +1742,10 @@ "message": "النطاق الأساسي", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "إسم النطاق", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "كشف المطابقة", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "الكشف الافتراضي عن المطابقة", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "خيارات التبديل" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "لا توجد كلمات مرور للعرض." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "إزالة" }, @@ -1677,6 +1899,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": "إجراء مهلة المخزن" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "كلمة المرور الرئيسية الجديدة لا تفي بمتطلبات السياسة العامة." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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": "لم يتم إعداد القياسات الحيوية" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "فشل القياسات الحيوية" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "النطاقات المستبعدة" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ نطاق غير صالح", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "إرسال", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Password protected" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Copy Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send created", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "تأكيد البريد الإلكتروني مطلوب" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "يجب عليك تأكيد بريدك الإلكتروني لاستخدام هذه الميزة. يمكنك تأكيد بريدك الإلكتروني في خزنة الويب." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "التسجيل التلقائي" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "إعدادات الملء التلقائي" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" + }, "autofillShortcut": { "message": "ملء تلقائي لاختصار لوحة المفاتيح" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "العناصر مع إعادة طلب كلمة المرور الرئيسية لا يمكن ملئها تلقائيًا عند تحميل الصفحة. التعبئة التلقائية عند تحميل الصفحة.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "ملء تلقائي عند تعيين تحميل الصفحة لاستخدام الإعداد الافتراضي.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "إيقاف تشغيل كلمة المرور الرئيسية مرة أخرى لتحرير هذا الحقل", @@ -2911,10 +3240,18 @@ "message": "افتح حسابك لعرض تسجيلات الدخول المطابقة", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "فتح الحساب", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "ملء بيانات الاعتماد لـ", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "إضافة عنصر مخزن جديد", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "تتوفر قائمة الملء التلقائي لBitwarden. اضغط على مفتاح السهم لأسفل للتحديد.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "قم بتشغيل Duo واتبع الخطوات لإنهاء تسجيل الدخول." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "كلمة مرور الملف غير صالحة، الرجاء استخدام كلمة المرور التي أدخلتها عند إنشاء ملف التصدير." }, - "importDestination": { - "message": "وجهة الاستيراد" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "تعرف على خيارات الاستيراد الخاصة بك" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "تأكيد" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index dbf060564fa..a6b2a35e133 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Güvənli anbarınıza müraciət etmək üçün giriş edin və ya yeni bir hesab yaradın." }, + "inviteAccepted": { + "message": "Dəvət qəbul edildi" + }, "createAccount": { "message": "Hesab yarat" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Bir parol təyin edərək hesabınızı yaratmağı başa çatdırın" }, - "login": { - "message": "Giriş et" - }, "enterpriseSingleSignOn": { "message": "Müəssisə üçün tək daxil olma" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Ana parol ipucu (ixtiyari)" }, + "joinOrganization": { + "message": "Təşkilata qoşul" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Bu ana parol təyin edərək bu təşkilata qoşulmağı tamamlayın." + }, "tab": { "message": "Vərəq" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Güvənlik kodunu kopyala" }, + "copyName": { + "message": "Adı kopyala" + }, + "copyCompany": { + "message": "Şirkəti kopyala" + }, + "copySSN": { + "message": "Sosial güvənlik nömrəsini kopyala" + }, + "copyPassportNumber": { + "message": "Pasport nömrəsini kopyala" + }, + "copyLicenseNumber": { + "message": "Lisenziya nömrəsini kopyala" + }, "autoFill": { "message": "Avto-doldurma" }, @@ -280,6 +301,24 @@ "editFolder": { "message": "Qovluğa düzəliş et" }, + "newFolder": { + "message": "Yeni qovluq" + }, + "folderName": { + "message": "Qovluq adı" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "Heç bir qovluq əlavə edilmədi" + }, + "createFoldersToOrganize": { + "message": "Anbar elementlərinizi təşkil etmək üçün qovluq yaradın" + }, + "deleteFolderPermanently": { + "message": "Bu qovluğu həmişəlik silmək istədiyinizə əminsiniz?" + }, "deleteFolder": { "message": "Qovluğu sil" }, @@ -345,16 +384,56 @@ "message": "Minimal parol uzunluğu" }, "uppercase": { - "message": "Böyük hərf (A-Z)" + "message": "Böyük hərf (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Kiçik hərf (a-z)" + "message": "Kiçik hərf (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Rəqəmlər (0-9)" + "message": "Rəqəmlər (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Xüsusi simvollar (!@#$%^&*)" + "message": "Xüsusi simvollar (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Daxil et", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Böyük hərf xarakterlərini daxil et", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Kiçik hərf xarakterlərini daxil et", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Rəqəmləri daxil et", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Xüsusi xarakterləri daxil et", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Söz sayı" @@ -376,7 +455,12 @@ "message": "Minimum simvol" }, "avoidAmbChar": { - "message": "Anlaşılmaz simvollardan çəkinin" + "message": "Anlaşılmaz simvollardan çəkinin", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Anlaşılmaz xarakterlərdən çəkin", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Anbarda axtar" @@ -556,6 +640,18 @@ "security": { "message": "Güvənlik" }, + "confirmMasterPassword": { + "message": "Ana parolu təsdiqlə" + }, + "masterPassword": { + "message": "Ana parol" + }, + "masterPassImportant": { + "message": "Unutsanız, ana parolunuz geri qaytarıla bilməz!" + }, + "masterPassHintLabel": { + "message": "Ana parol ipucusu" + }, "errorOccurred": { "message": "Bir xəta baş verdi" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Yeni hesabınız yaradıldı! İndi giriş edə bilərsiniz." }, + "newAccountCreated2": { + "message": "Yeni hesabınız yaradıldı!" + }, + "youHaveBeenLoggedIn": { + "message": "Giriş etdiniz!" + }, "youSuccessfullyLoggedIn": { "message": "Uğurla giriş etdiniz" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Doğrulama kodu tələb olunur." }, + "webauthnCancelOrTimeout": { + "message": "Kimlik doğrulama ləğv edildi və ya çox uzun çəkdi. Lütfən yenidən sınayın." + }, "invalidVerificationCode": { "message": "Yararsız doğrulama kodu" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Hazırkı veb sahifədən kimlik doğrulayıcı QR kodunu skan et" }, + "totpHelperTitle": { + "message": "2 addımlı doğrulama problemsiz edin" + }, + "totpHelper": { + "message": "Bitwarden, 2 addımlı doğrulama kodlarını saxlaya və doldura bilər. Açarı kopyalayıb bu xanaya yapışdırın." + }, + "totpHelperWithCapture": { + "message": "Bitwarden, 2 addımlı doğrulama kodlarını saxlaya və doldura bilər. Bu veb sayt kimlik doğrulayıcı QR kodunun ekran şəklini çəkmək üçün kamera ikonunu seçin, ya da açarı kopyalayıb bu xanaya yapışdırın." + }, + "learnMoreAboutAuthenticators": { + "message": "Kimlik doğrulayıcılar haqqında daha ətraflı" + }, "copyTOTP": { "message": "Kimlik doğrulayıcı açarını kopyala (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Giriş seansınızın müddəti bitdi." }, + "logIn": { + "message": "Giriş et" + }, + "restartRegistration": { + "message": "Qeydiyyatı yenidən başlat" + }, + "expiredLink": { + "message": "Vaxtı bitmiş keçid" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Lütfən qeydiyyatı yenidən başladın və ya giriş etməyə çalışın." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Artıq bir hesabınız ola bilər" + }, "logOutConfirmation": { "message": "Çıxış etmək istədiyinizə əminsiniz?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Yeni URI" }, + "addDomain": { + "message": "Domen əlavə et", + "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": { "message": "Element əlavə edildi" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Giriş əlavə etmək üçün soruş" }, + "vaultSaveOptionsTitle": { + "message": "Anbar seçimlərində saxla" + }, "addLoginNotificationDesc": { "message": "Anbarınızda tapılmayan bir elementin əlavə edilməsi soruşulsun." }, "addLoginNotificationDescAlt": { "message": "Anbarınızda tapılmayan bir elementin əlavə edilməsi soruşulsun. Giriş etmiş bütün hesablara aiddir." }, + "showCardsInVaultView": { + "message": "Kartları, Anbar görünüşündə Avto-doldurma təklifləri olaraq göstər" + }, "showCardsCurrentTab": { "message": "Kartları Vərəq səhifəsində göstər" }, "showCardsCurrentTabDesc": { "message": "Asan avto-doldurma üçün Vərəq səhifəsində kart elementlərini sadalayın." }, + "showIdentitiesInVaultView": { + "message": "Kimlikləri, Anbar görünüşündə Avto-doldurma təklifləri olaraq göstər" + }, "showIdentitiesCurrentTab": { "message": "Vərəq səhifəsində kimlikləri göstər" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "İlkin URI uyuşma aşkarlaması", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Avto-doldurma kimi əməliyyatları icra edərkən giriş etmə prosesi üçün URI uyuşma aşkarlamasının idarə edliəcəyi ilkin yolu seçin." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "Fayl qoşmaları üçün 1 GB şifrələnmiş saxlama sahəsi" }, + "premiumSignUpEmergency": { + "message": "Fövqəladə hal müraciəti" + }, "premiumSignUpTwoStepOptions": { "message": "YubiKey və Duo kimi mülkiyyətçi iki addımlı giriş seçimləri." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Premium üzvlüyü bitwarden.com veb anbarında satın ala bilərsiniz. İndi saytı ziyarət etmək istəyirsiniz?" }, + "premiumPurchaseAlertV2": { + "message": "Bitwarden veb tətbiqindəki hesab ayarlarınızda Premium satın ala bilərsiniz." + }, "premiumCurrentMember": { "message": "Premium üzvsünüz!" }, "premiumCurrentMemberThanks": { "message": "Bitwarden-i dəstəklədiyiniz üçün təşəkkürlər!" }, + "premiumFeatures": { + "message": "Premium-a yüksəlt və bunları əldə et:" + }, "premiumPrice": { "message": "Hamısı sadəcə ildə $PRICE$!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "Hamısı sadəcə ildə $PRICE$!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Təzələmə tamamlandı" }, @@ -1179,9 +1342,18 @@ }, "showAutoFillMenuOnFormFields": { "message": "Form xanalarında avto-doldurma menyusunu göstər", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Avto-doldurma təklifləri" + }, + "showInlineMenuLabel": { + "message": "Avto-doldurma təkliflərini form xanalarında göstər" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "İkon seçildikdə təklifləri göstər" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Giriş etmiş bütün hesablara aiddir." }, "turnOffBrowserBuiltInPasswordManagerSettings": { @@ -1202,15 +1374,34 @@ "message": "Avto-doldurma ikonu seçiləndə", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Səhifə yüklənəndə avto-doldur" + }, "enableAutoFillOnPageLoad": { "message": "Səhifə yüklənəndə avto-doldurmanı fəallaşdır" }, "enableAutoFillOnPageLoadDesc": { "message": "Giriş formu aşkarlananda, səhifə yüklənən zaman formu avto-doldurma icra edilsin." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Xəbərdarlıq:$CLOSETAG$ Təhlükəsizliyi pozulmuş və ya güvənilməyən veb saytlar, səhifə yüklənəndə avto-doldurmanı istifadə edə bilər.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Təhlükəli və ya güvənilməyən veb saytlar, səhifə yüklənərkən avto-doldurmanı istifadə edə bilər." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Risklər haqqında daha ətraflı" + }, "learnMoreAboutAutofill": { "message": "Avto-doldurma haqqında daha ətraflı" }, @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Anbarı yan çubuqda aç" }, - "commandAutofillDesc": { - "message": "Hazırkı veb sayt üçün son istifadə edilən giriş məlumatlarını avto-doldur" + "commandAutofillLoginDesc": { + "message": "Hazırkı veb sayt üçün son istifadə edilən girişi avto-doldur" + }, + "commandAutofillCardDesc": { + "message": "Hazırkı veb sayt üçün son istifadə edilən kartı avto-doldur" + }, + "commandAutofillIdentityDesc": { + "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" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Yoxlanış qutusu" + }, "cfTypeLinked": { "message": "Əlaqələndirildi", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "$TYPE$ - bax", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Parol tarixçəsi" }, @@ -1533,6 +1742,10 @@ "message": "Baza domeni", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Baza domeni (tövsiyə edilən)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domen adı", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Uyuşmanı aşkarlama", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "İlkin uyuşma aşkarlaması", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Seçimləri aç/bağla" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Sadalanacaq heç bir parol yoxdur." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Çıxart" }, @@ -1677,6 +1899,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Bir və ya daha çox təşkilat siyasəti yaradıcı ayarlarınıza təsir edir." }, + "passwordGenerator": { + "message": "Parol yaradıcı" + }, + "usernameGenerator": { + "message": "İstifadəçi adı yaradıcı" + }, + "useThisPassword": { + "message": "Bu parolu istifadə et" + }, + "useThisUsername": { + "message": "Bu istifadəçi adını istifadə et" + }, + "securePasswordGenerated": { + "message": "Güvənli parol yaradıldı! Həmçinin veb saytdakı parolunuzu güncəlləməyi unutmayın." + }, + "useGeneratorHelpTextPartOne": { + "message": "Yaradıcı istifadə et", + "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": "Daha unikal parollar yarat", + "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ı" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Yeni ana parolunuz siyasət tələblərini qarşılamır." }, - "receiveMarketingEmails": { - "message": "Elanlar, məsləhətlər və araşdırma fürsətləri üçün Bitwarden-dən e-poçt alın." + "receiveMarketingEmailsV2": { + "message": "Bitwarden-in tövsiyə, elan və araşdırma imkanlarını gələn qutunuzda əldə edin." }, "unsubscribe": { "message": "Abunəlikdən çıx" @@ -1874,6 +2119,12 @@ "nativeMessagingWrongUserTitle": { "message": "Hesablar uyuşmur" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometrik kilidini açma uğursuz oldu. Biometrik sirr açarı anbarın kilidini aça bilmədi. Lütfən biometriki yenidən qurmağa çalışın." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Biometrik açarı uyuşmazlığı" + }, "biometricsNotEnabledTitle": { "message": "Biometriklər qurulmayıb" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Lütfən bu istifadəçinin kilidini masaüstü tətbiqində açıb yenidən sınayın." }, + "biometricsNotAvailableTitle": { + "message": "Biometrik kilidi açma əlçatmazdır" + }, + "biometricsNotAvailableDesc": { + "message": "Biometrik kilidi açma hazırda əlçatmazdır. Lütfən daha sonra yenidən sınayın." + }, "biometricsFailedTitle": { "message": "Biometrik uğursuzdur" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "Bir təşkilat siyasəti, elementlərin fərdi anbarınıza köçürülməsini əngəllədi." }, + "domainsTitle": { + "message": "Domenlər", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "İstisna edilən domenlər" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden, giriş etmiş bütün hesablar üçün bu domenlərin giriş detallarını saxlamağı soruşmayacaq. Dəyişikliklərin qüvvəyə minməsi üçün səhifəni təzələməlisiniz." }, + "websiteItemLabel": { + "message": "Veb sayt $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ yararlı bir domen deyil", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "İstisna domen dəyişikliyi saxlanıldı" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Parolla qorunan" }, + "copyLink": { + "message": "Keçidi kopyala" + }, "copySendLink": { "message": "\"Send\" keçidini kopyala", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send yaradıldı", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send uğurla yaradıldı!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "Send, növbəti $DAYS$ ərzində keçidə sahib olan hər kəsə əlçatan olacaq.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send keçidi kopyalandı", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "\"Send\" saxlanıldı", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "E-poçt doğrulaması tələb olunur" }, + "emailVerifiedV2": { + "message": "E-poçt doğrulandı" + }, "emailVerificationRequiredDesc": { "message": "Bu özəlliyi istifadə etmək üçün e-poçtunuzu doğrulamalısınız. E-poçtunuzu veb anbarında doğrulaya bilərsiniz." }, @@ -2179,6 +2476,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ış 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": "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" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Avto-doldurma ayarları" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Avto-doldurma qısayolu" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Qısayolu dəyişdir" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Qısayolları idarə et" + }, "autofillShortcut": { "message": "Avto-doldurma klaviatura qısayolu" }, - "autofillShortcutNotSet": { - "message": "Avto-doldurma qısayolu ayarlanmayıb. Bunu brauzerin ayarlarında dəyişdirin." + "autofillLoginShortcutNotSet": { + "message": "Avto-doldurma giriş qısayolu ayarlanmayıb. Bunu brauzerin ayarlarında dəyişdirin." }, - "autofillShortcutText": { - "message": "Avto-doldurma qısayolu: $COMMAND$. Bunu brauzerin ayarlarında dəyişdirin.", + "autofillLoginShortcutText": { + "message": "Girişin avto-doldurma qısayolu: $COMMAND$. Bütün qısayolları brauzerin ayarlarında idarə edin.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Cihaz güvənlidir" }, + "sendsNoItemsTitle": { + "message": "Aktiv \"Send\" yoxdur", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Şifrələnmiş məlumatları hər kəslə güvənli şəkildə paylaşmaq üçün \"Send\"i istifadə edin.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Giriş lazımdır." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 xananın diqqətinizə ehtiyacı var." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ xananın diqqətinizə ehtiyacı var.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Seç --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "\"Ana parolu təkrar soruş\" özəlliyi olan elementlər səhifə yüklənəndə avto-doldurulmur. \"Səhifə yüklənəndə avto-doldurma\" özəlliyi söndürülüb.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "\"Səhifə yüklənəndə avto-doldurma\" özəlliyi ilkin ayarı istifadə etmək üzrə ayarlandı.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Bu xanaya düzəliş etmək üçün \"Ana parolu təkrar soruş\"u söndürün", @@ -2911,10 +3240,18 @@ "message": "Uyuşan giriş məlumatlarına baxmaq üçün hesabınızın kilidini açın", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Avto-doldurma təkliflərinə baxmaq üçün hesabınızın kilidini açın", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Hesabın kilidini aç", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Hesabınızın kilidini açın, yeni bir pəncərədə açılır", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Kimlik məlumatlarını doldur", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Yeni anbar elementi əlavə et", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "Yeni giriş", + "description": "Button text to display within inline menu when there are no matching items on a login field" + }, + "addNewLoginItemAria": { + "message": "Yeni anbar giriş elementini əlavə et, yeni bir pəncərədə açılır", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "Yeni kart", + "description": "Button text to display within inline menu when there are no matching items on a credit card field" + }, + "addNewCardItemAria": { + "message": "Yeni anbar kart elementini əlavə et, yeni bir pəncərədə açılır", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "Yeni kimlik", + "description": "Button text to display within inline menu when there are no matching items on an identity field" + }, + "addNewIdentityItemAria": { + "message": "Yeni anbar kimlik elementini əlavə et, yeni bir pəncərədə açılır", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Bitwarden avto-doldurma menyusu mövcuddur. Seçmək üçün aşağı ox düyməsinə basın.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Duo xidmətinə bağlanarkən xəta baş verdi. Fərqli iki addımlı giriş üsulu istifadə edin və ya kömək üçün Duo ilə əlaqə saxlayın." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Duo-nu başladın və giriş prosesini tamamlamaq üçün addımları izləyin." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Yararsız fayl parolu, lütfən xaricə köçürmə faylını yaradarkən daxil etdiyiniz parolu istifadə edin." }, - "importDestination": { - "message": "Hədəfi daxilə köçür" + "destination": { + "message": "Hədəf" }, "learnAboutImportOptions": { "message": "Daxilə köçürmə seçimlərinizi öyrənin" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Başladan saytın tələb etdiyi doğrulama. Bu özəllik, hələlik ana parolu olmayan hesablara tətbiq olunmur." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Keçid açarı ilə giriş edilsin?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "Bu sayt üçün uyuşan bir giriş məlumatınız yoxdur." }, + "noMatchingLoginsForSite": { + "message": "Bu sayt üçün uyuşan giriş məlumatı yoxdur" + }, "confirm": { "message": "Təsdiqlə" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Keçid açarını yeni bir giriş olaraq saxla" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Bu keçid açarını saxlayacaq bir giriş seçin" }, + "chooseCipherForPasskeyAuth": { + "message": "Giriş ediləcək keçid açarını seçin" + }, "passkeyItem": { "message": "Keçid açarı elementi" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,6 +3659,30 @@ "message": "Ortaq formatlar", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Brauzer ayarları ilə davam edilsin?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Kömək Mərkəzi ilə davam edilsin?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" + }, + "confirmContinueToHelpCenterPasswordManagementContent": { + "message": "Brauzerinizin avto-doldurma və parol idarəsi ayarlarını dəyişdirin.", + "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": "Uzantı qısayollarını brauzerinizin ayarlarında görə və ayarlaya bilərsiniz.", + "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": "Brauzerinizin avto-doldurma və parol idarəsi ayarlarını dəyişdirin.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" + }, + "confirmContinueToBrowserKeyboardShortcutSettingsContent": { + "message": "Uzantı qısayollarını brauzerinizin ayarlarında görə və ayarlaya bilərsiniz.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Bitwarden ilkin parol meneceriniz olaraq təyin edilsin?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" @@ -3317,10 +3711,18 @@ "message": "Kimlik məlumatları uğurla saxlanıldı!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Parol saxlanıldı!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Kimlik məlumatları uğurla güncəlləndi!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Parol güncəlləndi!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Kimlik məlumatlarını saxlama xətası. Detallar üçün konsolu yoxlayın.", "description": "Notification message for when saving credentials has failed." @@ -3334,20 +3736,6 @@ "passkeyRemoved": { "message": "Parol silindi" }, - "unassignedItemsBannerNotice": { - "message": "Bildiriş: Təyin edilməyən təşkilat elementləri artıq Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Bildiriş: 16 May 2024-cü il tarixindən etibarən, təyin edilməyən təşkilat elementləri Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Bu elementləri görünən etmək üçün", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "bir kolleksiyaya təyin edin.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Avto-doldurma təklifləri" }, @@ -3415,7 +3803,7 @@ }, "autofillTitle": { "message": "Avto-doldur - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "Kopyalanacaq dəyər yoxdur" }, - "assignCollections": { - "message": "Kolleksiyaları təyin et" + "assignToCollections": { + "message": "Kolleksiyalara təyin et" }, "copyEmail": { "message": "E-poçtu kopyala" @@ -3493,13 +3881,13 @@ "message": "Qovluğu olmayan elementlər" }, "itemDetails": { - "message": "Item details" + "message": "Element detalları" }, "itemName": { - "message": "Item name" + "message": "Element adı" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "\"Yalnız baxma\" icazələrinə sahib kolleksiyaları silə bilməzsiniz: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3899,33 @@ "message": "Təşkilat deaktiv edildi" }, "owner": { - "message": "Owner" + "message": "Sahiblik" }, "selfOwnershipLabel": { - "message": "You", + "message": "Siz", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Deaktiv edilmiş təşkilatlardakı elementlərə müraciət edilə bilməz. Kömək üçün təşkilatınızın sahibi ilə əlaqə saxlayın." }, + "additionalInformation": { + "message": "Əlavə məlumat" + }, + "itemHistory": { + "message": "Element tarixçəsi" + }, + "lastEdited": { + "message": "Son düzəliş" + }, + "ownerYou": { + "message": "Sahiblik: Siz" + }, + "linked": { + "message": "Əlaqələndirildi" + }, + "copySuccessful": { + "message": "Uğurla kopyalandı" + }, "upload": { "message": "Yüklə" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filtrlər" + }, + "personalDetails": { + "message": "Şəxsi detallar" + }, + "identification": { + "message": "İdentifikasiya" + }, + "contactInfo": { + "message": "Əlaqə məlumatı" + }, + "downloadAttachment": { + "message": "$ITEMNAME$ - endir", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "kart nömrəsinin sonu", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Giriş məlumatları" + }, + "authenticatorKey": { + "message": "Kimlik doğrulayıcı açarı" + }, + "autofillOptions": { + "message": "Avto-doldurma seçimləri" + }, + "websiteUri": { + "message": "Veb sayt (URI)" + }, + "websiteUriCount": { + "message": "Veb sayt (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": "Veb sayt əlavə edildi" + }, + "addWebsite": { + "message": "Veb sayt əlavə et" + }, + "deleteWebsite": { + "message": "Veb saytı sil" + }, + "defaultLabel": { + "message": "İlkin ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "$WEBSITE$ ilə uyuşma aşkarlamasını göstər", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "$WEBSITE$ ilə uyuşma aşkarlamasını gizlət", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Səhifə yüklənəndə avto-doldurulsun?" + }, + "cardExpiredTitle": { + "message": "Vaxtı bitmiş kart" + }, + "cardExpiredMessage": { + "message": "Yeniləmisinizsə, kart məlumatlarınızı güncəlləyin" + }, + "cardDetails": { + "message": "Kart detalları" + }, + "cardBrandDetails": { + "message": "$BRAND$ detalları", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Animasiyaları fəallaşdır" + }, + "addAccount": { + "message": "Hesab əlavə et" + }, + "loading": { + "message": "Yüklənir" + }, + "data": { + "message": "Data" + }, + "passkeys": { + "message": "Keçid açarı", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Parollar", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Keçid açarı ilə giriş et", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, + "assign": { + "message": "Təyin et" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Yalnız bu kolleksiyalara müraciəti olan təşkilat üzvləri bu elementi görə biləcək." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Yalnız bu kolleksiyalara müraciəti olan təşkilat üzvləri bu elementləri görə biləcək." + }, + "bulkCollectionAssignmentWarning": { + "message": "$TOTAL_COUNT$ element seçmisiniz. Düzəliş icazəniz olmadığı üçün $READONLY_COUNT$ elementi güncəlləyə bilməzsiniz.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Xana əlavə et" + }, + "add": { + "message": "Əlavə et" + }, + "fieldType": { + "message": "Xana növü" + }, + "fieldLabel": { + "message": "Xana etiketi" + }, + "textHelpText": { + "message": "Təhlükəsizlik sualları kimi datalar üçün mətn xanalarını istifadə edin" + }, + "hiddenHelpText": { + "message": "Parol kimi həssas datalar üçün gizli xanaları istifadə edin" + }, + "checkBoxHelpText": { + "message": "\"E-poçtu xatırla\" kimi formun təsdiq qutusunu avto-doldurmaq istəyirsinizsə təsdiq qutularını istifadə edin" + }, + "linkedHelpText": { + "message": "Müəyyən bir veb sayt üçün avto-doldurma problemləri ilə üzləşdikdə əlaqələndirilmiş xana istifadə edin." + }, + "linkedLabelHelpText": { + "message": "Xana üçün bunları daxil edin: kimlik, ad, aria-label və ya placeholder." + }, + "editField": { + "message": "Xanaya düzəliş et" + }, + "editFieldLabel": { + "message": "$LABEL$ - düzəliş et", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "$LABEL$ - sil", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ əlavə edildi", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "$LABEL$ - yenidən sırala. Ox düyməsi ilə elementi yuxarı və ya aşağı daşıyın.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ yuxarı daşındı, mövqeyi $INDEX$/$LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Təyin ediləcək kolleksiyaları seçin" + }, + "personalItemTransferWarningSingular": { + "message": "1 element seçilmiş təşkilata birdəfəlik transfer ediləcək. Artıq bu elementlərə sahib olmaya bilməyəcəksiniz." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ element seçilmiş təşkilata birdəfəlik transfer ediləcək. Artıq bu elementlərə sahib olmaya bilməyəcəksiniz.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 element $ORG$ təşkilatına birdəfəlik transfer ediləcək. Artıq bu elementə sahib olmaya bilməyəcəksiniz.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ element $ORG$ təşkilatına birdəfəlik transfer ediləcək. Artıq bu elementlərə sahib olmaya bilməyəcəksiniz.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Uğurla təyin edilən kolleksiyalar" + }, + "nothingSelected": { + "message": "Heç nə seçməmisiniz." + }, + "movedItemsToOrg": { + "message": "Seçilən elementlər $ORGNAME$ təşkilatına daşınıldı", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Elementlər bura daşındı: $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Element bura daşındı: $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ aşağı daşındı, mövqeyi $INDEX$/$LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Element yeri" + }, + "fileSends": { + "message": "Fayl \"Send\"ləri" + }, + "textSends": { + "message": "Mətn \"Send\"ləri" + }, + "bitwardenNewLook": { + "message": "Bitwarden-in yeni bir görünüşü var!" + }, + "bitwardenNewLookDesc": { + "message": "Anbar vərəqindən avto-doldurma və axtarış etmə artıq daha asan və intuitivdir. Nəzər salın!" + }, + "accountActions": { + "message": "Hesab fəaliyyətləri" + }, + "showNumberOfAutofillSuggestions": { + "message": "Uzantı ikonunda giriş üçün avto-doldurma təklif sayını göstər" + }, + "systemDefault": { + "message": "İlkin sistem" + }, + "enterprisePolicyRequirementsApplied": { + "message": "Müəssisə siyasət tələbləri bu ayara tətbiq edildi" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Xarakter sayını göstər" + }, + "hideCharacterCount": { + "message": "Xarakter sayını gizlət" + }, + "itemsInTrash": { + "message": "Tullantıdakı elementlər" + }, + "noItemsInTrash": { + "message": "Tullantıda element yoxdur" + }, + "noItemsInTrashDesc": { + "message": "Sildiyiniz elementlər burada görünəcək və 30 gün sonra birdəfəlik silinəcək" + }, + "trashWarning": { + "message": "Tullantıda 30 gündən çox qalan elementlər avtomatik silinəcək" + }, + "restore": { + "message": "Bərpa et" + }, + "deleteForever": { + "message": "Həmişəlik sil" + }, + "noEditPermissions": { + "message": "Bu elementə düzəliş etmə icazəniz yoxdur" } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index e3936387e05..cf14c8e32a0 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Увайдзіце або стварыце новы ўліковы запіс для доступу да бяспечнага сховішча." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "Стварыць уліковы запіс" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "Увайсці" - }, "enterpriseSingleSignOn": { "message": "Адзіны ўваход прадпрыемства (SSO)" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Падказка да асноўнага пароля (неабавязкова)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "Укладка" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Скапіяваць код бяспекі" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "Аўтазапаўненне" }, @@ -280,6 +301,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,16 +384,56 @@ "message": "Мінімальная даўжыня пароля" }, "uppercase": { - "message": "Вялікія літары (A-Z)" + "message": "Вялікія літары (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Маленькія літары (a-z)" + "message": "Маленькія літары (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Лічбы (0-9)" + "message": "Лічбы (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Спецыяльныя сімвалы (!@#$%^&*)" + "message": "Спецыяльныя сімвалы (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Колькасць слоў" @@ -376,7 +455,12 @@ "message": "Мінімум спецыяльных сімвалаў" }, "avoidAmbChar": { - "message": "Пазбягаць неадназначных сімвалаў" + "message": "Пазбягаць неадназначных сімвалаў", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Пошук у сховішчы" @@ -556,6 +640,18 @@ "security": { "message": "Бяспека" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Адбылася памылка" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Ваш уліковы запіс створаны! Цяпер вы можаце ўвайсці ў яго." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "Вы паспяхова аўтарызаваны" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Патрабуецца праверачны код." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Памылковы праверачны код" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Тэрмін дзеяння вашага сеансу завяршыўся." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Вы ўпэўнены, што хочаце выйсці?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Новы URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "Элемент дададзены" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Пытацца пры дадаванні лагіна" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "Пытацца пра дадаванне элемента, калі ён адсутнічае ў вашым сховішчы." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Паказваць карткі на старонцы з укладкамі" }, "showCardsCurrentTabDesc": { "message": "Спіс элементаў картак на старонцы з укладкамі для лёгкага аўтазапаўнення." }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" + }, "showIdentitiesCurrentTab": { "message": "Паказваць пасведчанні на старонцы з укладкамі" }, @@ -791,7 +936,7 @@ "message": "Абнавіць" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Разблакіраваць" @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Прадвызначанае выяўленне супадзення URI", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Выберыце прадвызначаны спосаб вызначэння адпаведнасці URI для лагінаў пры выкананні такіх дзеянняў, як аўтаматычнае запаўненне." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 ГБ зашыфраванага сховішча для далучаных файлаў." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Вы можаце купіць прэміяльны статус на bitwarden.com. Перайсці на вэб-сайт зараз?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "У вас прэміяльны статус!" }, "premiumCurrentMemberThanks": { "message": "Дзякуй за падтрымку Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "Усяго за $PRICE$ у год!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Абнаўленне завершана" }, @@ -1178,14 +1341,23 @@ "message": "URL-адрас сервера асяроддзя захаваны." }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,18 +1371,37 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "Аўтазапаўненне пры загрузцы старонкі" }, "enableAutoFillOnPageLoadDesc": { "message": "Калі выяўлена форма ўваходу, то будзе выканана яе аўтазапаўненне падчас загрузкі вэб-старонкі." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Скампраметаваныя або ненадзейныя вэб-сайты могуць задзейнічаць функцыю аўтазапаўнення падчас загрузкі старонкі." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" + }, "learnMoreAboutAutofill": { "message": "Даведацца больш пра аўтазапаўненне" }, @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Адкрыць сховішча ў бакавой панэлі" }, - "commandAutofillDesc": { - "message": "Аўтазапаўненне апошняга скарыстанага лагіна для бягучага вэб-сайта" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Генерыраваць і скапіяваць новы выпадковы пароль у буфер абмену" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Булева" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Звязана", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Гісторыя пароляў" }, @@ -1533,6 +1742,10 @@ "message": "Асноўны дамен", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Імя дамена", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Выяўленне супадзенняў", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Прадвызначаны метад выяўлення", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Пераключыць параметры" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "У спісе адсутнічаюць паролі." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Выдаліць" }, @@ -1677,6 +1899,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": "Дзеянне пасля заканчэння часу чакання сховішча" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Ваш новы асноўны пароль не адпавядае патрабаванням палітыкі." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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": "Біяметрыя не ўключана" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Выключаныя дамены" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ не з'яўляецца правільным даменам", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Абаронена паролем" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Скапіяваць спасылку на Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Створаны Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send адрэдагаваны", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Патрабуецца праверка электроннай пошты" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Вы павінны праверыць свой адрас электроннай пошты, каб выкарыстоўваць гэту функцыю. Зрабіць гэта можна ў вэб-сховішчы." }, @@ -2179,6 +2476,9 @@ "updateWeakMasterPasswordWarning": { "message": "Ваш асноўны пароль не адпавядае адной або некалькім палітыкам арганізацыі. Для атрымання доступу да сховішча, вы павінны абнавіць яго. Працягваючы, вы выйдзіце з бягучага сеанса і вам неабходна будзе ўвайсці паўторна. Актыўныя сеансы на іншых прыладах могуць заставацца актыўнымі на працягу адной гадзіны." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "resetPasswordPolicyAutoEnroll": { "message": "Аўтаматычная рэгістрацыя" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Налады аўтазапаўнення" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" + }, "autofillShortcut": { "message": "Спалучэнні клавіш аўтазапаўнення" }, - "autofillShortcutNotSet": { - "message": "Спалучэнні клавіш аўтазапаўнення не зададзены. Змяніце гэта ў наладах браўзера." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "Спалучэнні клавіш аўтазапаўнення: $COMMAND$. Змяніце гэта ў наладах браўзера.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Выбраць --" }, @@ -2878,12 +3207,12 @@ "message": "Мянушка дамена" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Даведацца пра параметры імпартавання" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Пацвердзіць" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 6cfb1ee08bd..9d90293b0ca 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Впишете се или създайте нов абонамент, за да достъпите защитен трезор." }, + "inviteAccepted": { + "message": "Поканата е приета" + }, "createAccount": { "message": "Създаване на акаунт" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Завършете регистрацията си като зададете парола" }, - "login": { - "message": "Вписване" - }, "enterpriseSingleSignOn": { "message": "Еднократна идентификация (SSO)" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Подсказване за главната парола (по избор)" }, + "joinOrganization": { + "message": "Присъединяване към организацията" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Завършете присъединяването си към тази организация като зададете главна парола." + }, "tab": { "message": "Раздел" }, @@ -102,11 +108,26 @@ "message": "Копиране на потребителското име" }, "copyNumber": { - "message": "Копиране на но̀мера" + "message": "Копиране на номера" }, "copySecurityCode": { "message": "Копиране на кода за сигурност" }, + "copyName": { + "message": "Копиране на името" + }, + "copyCompany": { + "message": "Копиране на компанията" + }, + "copySSN": { + "message": "Копиране на номера на осигуровката" + }, + "copyPassportNumber": { + "message": "Копиране на номера на паспорта" + }, + "copyLicenseNumber": { + "message": "Копиране на номера на свидетелството" + }, "autoFill": { "message": "Автоматично дописване" }, @@ -280,6 +301,24 @@ "editFolder": { "message": "Редактиране на папка" }, + "newFolder": { + "message": "Нова папка" + }, + "folderName": { + "message": "Име на папката" + }, + "folderHintText": { + "message": "Можете да вложите една папка в друга като въведете името на горната папка, а след това „/“. Пример: Социални/Форуми" + }, + "noFoldersAdded": { + "message": "Няма добавени папки" + }, + "createFoldersToOrganize": { + "message": "Създавайте папки, за да организирате елементите в трезора си" + }, + "deleteFolderPermanently": { + "message": "Наистина ли искате да изтриете тази папка окончателно?" + }, "deleteFolder": { "message": "Изтриване на папка" }, @@ -345,16 +384,56 @@ "message": "Минимална дължина на паролата" }, "uppercase": { - "message": "Главни букви (A-Z)" + "message": "Главни букви (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Малки букви (a-z)" + "message": "Малки букви (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Числа (0-9)" + "message": "Числа (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Специални знаци (!@#$%^&*)" + "message": "Специални знаци (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Включване", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Включване на главни букви", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Включване на малки букви", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Включване на цифри", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Включване на специални знаци", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Брой думи" @@ -376,7 +455,12 @@ "message": "Минимален брой специални знаци" }, "avoidAmbChar": { - "message": "Без нееднозначни знаци" + "message": "Без нееднозначни знаци", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Без нееднозначни знаци", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Търсене в трезора" @@ -556,6 +640,18 @@ "security": { "message": "Сигурност" }, + "confirmMasterPassword": { + "message": "Потвърждаване на главната парола" + }, + "masterPassword": { + "message": "Главна парола" + }, + "masterPassImportant": { + "message": "Главната парола не може да бъде възстановена, ако я забравите!" + }, + "masterPassHintLabel": { + "message": "Подсказка за главната парола" + }, "errorOccurred": { "message": "Възникна грешка" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Абонаментът ви бе създаден. Вече можете да се впишете." }, + "newAccountCreated2": { + "message": "Новата Ви регистрация беше създадена!" + }, + "youHaveBeenLoggedIn": { + "message": "Вече сте вписан(а)!" + }, "youSuccessfullyLoggedIn": { "message": "Вписахте се успешно" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Кодът за потвърждение е задължителен." }, + "webauthnCancelOrTimeout": { + "message": "Удостоверяването беше отменено или отне твърде много време. Моля, опитайте отново." + }, "invalidVerificationCode": { "message": "Грешен код за потвърждаване" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Сканирайте QR-кода за удостоверяване от текущата страница" }, + "totpHelperTitle": { + "message": "Направете 2-стъпковото удостоверяване безпроблемно и незабележимо" + }, + "totpHelper": { + "message": "Битуорден може да съхранява и попълва кодовете за 2-стъпково потвърждаване. Копирайте ключа в това поле." + }, + "totpHelperWithCapture": { + "message": "Битуорден може да съхранява и попълва кодовете за 2-стъпково потвърждаване. Изберете иконката с камера, за да направите екранна снимка на QR-кода от този уеб сайт или копирайте ключа в това поле." + }, + "learnMoreAboutAuthenticators": { + "message": "Научете повече за средствата за удостоверяване" + }, "copyTOTP": { "message": "Копиране на удостоверителния ключ (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Сесията ви изтече." }, + "logIn": { + "message": "Вписване" + }, + "restartRegistration": { + "message": "Рестартиране на регистрацията" + }, + "expiredLink": { + "message": "Връзка с изтекла давност" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Рестартирайте регистрацията или опитайте да се впишете." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Може вече да имате регистрация" + }, "logOutConfirmation": { "message": "Сигурни ли сте, че искате да се отпишете?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Нов унифициран идентификатор на ресурс" }, + "addDomain": { + "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": { "message": "Елементът е добавен" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Питане за добавяне на запис" }, + "vaultSaveOptionsTitle": { + "message": "Запазване в настройките на трезора" + }, "addLoginNotificationDesc": { "message": "Известията за запазване на регистрации автоматично ви подканят да запазите новите регистрации в трезора при първото ви вписване в тях." }, "addLoginNotificationDescAlt": { "message": "Питане за добавяне на елемент, ако такъв не бъде намерен в трезора. Прилага се за всички регистрации, в които сте вписан(а)." }, + "showCardsInVaultView": { + "message": "Показване на картите като предложения за авт. попълване в изгледа на трезора" + }, "showCardsCurrentTab": { "message": "Показване на карти в страницата с разделите" }, "showCardsCurrentTabDesc": { "message": "Показване на картите в страницата с разделите, за лесно автоматично попълване." }, + "showIdentitiesInVaultView": { + "message": "Показване на самоличности като предложения за самопопълване в изгледа на трезора" + }, "showIdentitiesCurrentTab": { "message": "Показване на самоличности в страницата с разделите" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Стандартно засичане на адреси", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Какъв да е начинът, по който да се засича съответствие на адреса за автоматичното попълване на данните." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB пространство за файлове, които се шифрират." }, + "premiumSignUpEmergency": { + "message": "Авариен достъп" + }, "premiumSignUpTwoStepOptions": { "message": "Частно двустепенно удостоверяване чрез YubiKey и Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Може да платите абонамента си през сайта bitwarden.com. Искате ли да го посетите сега?" }, + "premiumPurchaseAlertV2": { + "message": "Можете да закупите платената версия от настройките на регистрацията си, в приложението по уеб на Битуорден." + }, "premiumCurrentMember": { "message": "Честито, ползвате платен абонамент!" }, "premiumCurrentMemberThanks": { "message": "Благодарим ви за подкрепата на Bitwarden." }, + "premiumFeatures": { + "message": "Преминете към платената версия и ще получите:" + }, "premiumPrice": { "message": "И това само за $PRICE$ на година!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "И това само за $PRICE$ на година!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Абонаментът е опреснен" }, @@ -1179,9 +1342,18 @@ }, "showAutoFillMenuOnFormFields": { "message": "Показване на меню за авт. попълване при полетата на формулярите", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Предложения за авт. попълване" + }, + "showInlineMenuLabel": { + "message": "Показване на предложения за авт. попълване на полетата във формуляри" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Показване на предложения когато иконката е избрана" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Прилага се за всички регистрации, в които сте вписан(а)." }, "turnOffBrowserBuiltInPasswordManagerSettings": { @@ -1202,15 +1374,34 @@ "message": "Когато бъде избрана иконката за авт. попълване", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Автоматично попълване при зареждане на страницата" + }, "enableAutoFillOnPageLoad": { "message": "Включване на автоматичното попълване" }, "enableAutoFillOnPageLoadDesc": { "message": "При засичане на формуляр за вписване при зареждането на уеб страницата автоматично да се попълват данните на съответстващата регистрация." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Внимание:$CLOSETAG$ Опасните или компроментирани уеб сайтове могат да се възползват от функционалността за автоматично попълване при зареждане на страницата.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Компроментирани и измамни уеб сайтове могат да се възползват от автоматичното попълване при зареждане на страницата." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Научете повече относно рисковете" + }, "learnMoreAboutAutofill": { "message": "Научете повече относно автоматичното попълване" }, @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Отваряне на трезора в страничната лента" }, - "commandAutofillDesc": { - "message": "Автоматично попълване на последно използвания запис в текущия сайт." + "commandAutofillLoginDesc": { + "message": "Автоматично попълване на последно използвания запис в текущия уеб сайт" + }, + "commandAutofillCardDesc": { + "message": "Автоматично попълване на последно използваната карта в текущия уеб сайт" + }, + "commandAutofillIdentityDesc": { + "message": "Автоматично попълване на последно използваната самоличност в текущия уеб сайт" }, "commandGeneratePasswordDesc": { "message": "Създаване и копиране на нова случайна парола в буфера." @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Булево" }, + "cfTypeCheckbox": { + "message": "Поле за отметка" + }, "cfTypeLinked": { "message": "Свързано", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1376,7 +1576,7 @@ "message": "Д-р" }, "mx": { - "message": "Mx" + "message": "Госпоуи" }, "firstName": { "message": "Собствено име" @@ -1454,7 +1654,7 @@ "message": "Самоличност" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "Ново $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "Преглед на $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Хронология на паролата" }, @@ -1533,6 +1742,10 @@ "message": "Основен домейн", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Основен домейн (препоръчително)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Име на домейн", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Откриване на съвпадения", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Стандартно откриване на съвпадения", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Настройки на превключване" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Няма пароли за показване." }, + "clearHistory": { + "message": "Изчистване на историята" + }, + "noPasswordsToShow": { + "message": "Няма пароли за показване" + }, + "noRecentlyGeneratedPassword": { + "message": "Скоро не сте генерирали пароли" + }, "remove": { "message": "Премахване" }, @@ -1677,6 +1899,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": "Действие при изтичане на времето" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Паролата ви не отговаря на политиките." }, - "receiveMarketingEmails": { - "message": "Получавайте е-писма от Битоурден за новини, съвети и възможности за проучвания." + "receiveMarketingEmailsV2": { + "message": "Получавайте съвети, обявления и предложения за участие в проучвания от Битуорден в пощенската си кутия." }, "unsubscribe": { "message": "Отписване" @@ -1874,6 +2119,12 @@ "nativeMessagingWrongUserTitle": { "message": "Регистрациите са различни" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Отключването чрез биометрични данни не беше успешно. Биометричният таен ключ не успя да отключи трезора. Опитайте да направите настройката на биометричните данни отново." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Несъответстващ биометричен ключ" + }, "biometricsNotEnabledTitle": { "message": "Потвърждаването с биометрични данни не е включено" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Отключете потребителя в настолното приложение и опитайте отново." }, + "biometricsNotAvailableTitle": { + "message": "Отключването чрез биометрични данни не е налично" + }, + "biometricsNotAvailableDesc": { + "message": "Отключването чрез биометрични данни не е налично в момента. Опитайте отново по-късно." + }, "biometricsFailedTitle": { "message": "Неуспешно удостоверяване чрез биометрични данни" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "Политика на организацията забранява да внасяте елементи в личния си трезор." }, + "domainsTitle": { + "message": "Домейни", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Изключени домейни" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Битуорден няма да пита дали да запазва данните за вход в тези сайтове за всички регистрации, в които сте вписан(а). За да влезе правилото в сила, презаредете страницата." }, + "websiteItemLabel": { + "message": "Уеб сайт $number$ (адрес)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ не е валиден домейн", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Промените на изключените домейни са запазени" + }, "send": { "message": "Изпращане", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Защита с парола" }, + "copyLink": { + "message": "Копиране на връзката" + }, "copySendLink": { "message": "Копиране на връзката към изпратеното", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Създадено изпращане", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Изпращането е създадено успешно!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "Изпращането ще бъде достъпно за всеки с връзката през следващите $DAYS$ дни.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Връзката към Изпращането е копирана", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Редактирано изпращане", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Изисква се потвърждение на е-пощата" }, + "emailVerifiedV2": { + "message": "Е-пощата е потвърдена" + }, "emailVerificationRequiredDesc": { "message": "Трябва да потвърдите е-пощата си, за да използвате тази функционалност. Можете да го направите в уеб-трезора." }, @@ -2179,6 +2476,9 @@ "updateWeakMasterPasswordWarning": { "message": "Вашата главна парола не отговаря на една или повече политики на организацията Ви. За да получите достъп до трезора, трябва да промените главната си парола сега. Това означава, че ще бъдете отписан(а) от текущата си сесия и ще трябва да се впишете отново. Активните сесии на други устройства може да продължат да бъдат активни още един час." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Вашата организация е деактивирала шифроването чрез доверени устройства. Задайте главна парола, за да получите достъп до трезора си." + }, "resetPasswordPolicyAutoEnroll": { "message": "Автоматично включване" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Настройки за автоматичното попълване" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Клавишна комбинация за авт. попълване" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Промяна на клавишната комбинация" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Управление на клавишните комбинации" + }, "autofillShortcut": { "message": "Клавишна комбинация за автоматично попълване" }, - "autofillShortcutNotSet": { - "message": "Няма зададена клавишна комбинация за автоматичното попълване. Променете това в настройките на браузъра." + "autofillLoginShortcutNotSet": { + "message": "Няма зададена клавишна комбинация за автоматично попълване на данни за вписване. Променете това в настройките на браузъра." }, - "autofillShortcutText": { - "message": "Клавишната комбинация за автоматично попълване е: $COMMAND$. Може да я промените в настройките на браузъра.", + "autofillLoginShortcutText": { + "message": "Клавишната комбинация за автоматично попълване на данни за вписване е $COMMAND$. Може да променяте всички клавишни комбинации в настройките на браузъра.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Устройството е доверено" }, + "sendsNoItemsTitle": { + "message": "Няма активни Изпращания", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Използвайте Изпращане, за да споделите безопасно шифрована информация с някого.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Полето е задължително да бъде попълнено." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 поле се нуждае от вниманието Ви." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ полета се нуждаят от вниманието Ви.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Изберете --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Записите с включено изискване за повторно въвеждане на главната парола не могат да бъдат попълвани автоматично при зареждане на страницата. Автоматичното попълване при зареждане на страницата е изключено.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "Автоматичното попълване при зареждане на страницата използва настройката си по подразбиране.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Изключете повторното въвеждане на главната парола, за да редактирате това поле", @@ -2911,10 +3240,18 @@ "message": "Отключете регистрацията си, за да видите съвпадащите записи за вписване", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Отключете регистрацията си, за да видите предложение за автоматично попълване", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Отключване на регистрацията", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Отклюване на регистрацията, отваря се в нов прозорец", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Попълване на данните за", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Добавяне на нов елемент в трезора", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "Нов елемент за вписване", + "description": "Button text to display within inline menu when there are no matching items on a login field" + }, + "addNewLoginItemAria": { + "message": "Добавяне на нов елемент за вписване в трезора, отваря се в нов прозорец", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "Нова карта", + "description": "Button text to display within inline menu when there are no matching items on a credit card field" + }, + "addNewCardItemAria": { + "message": "Добавяне на нов елемент за карта в трезора, отваря се в нов прозорец", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "Нова самоличност", + "description": "Button text to display within inline menu when there are no matching items on an identity field" + }, + "addNewIdentityItemAria": { + "message": "Добавяне на нов елемент за идентичност в трезора, отваря се в нов прозорец", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Има налично меню за авт. попълване на Битуорден. Натиснете стрелката надолу, за да го изберете.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Грешка при свързването с услугата на Duo. Използвайте друг метод за двустепенно удостоверяване или се свържете с Duo за съдействие." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Стартирайте DUO и следвайте инструкциите, за да завършите вписването." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Неправилна парола за файла. Използвайте паролата, която сте въвели при създаването на изнесения файл." }, - "importDestination": { - "message": "Място на внасяне" + "destination": { + "message": "Местоназначение" }, "learnAboutImportOptions": { "message": "Научете повече относно възможностите за внасяне" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Изисква се проверка от иницииращия сайт. Тази функция все още не е внедрена за акаунти без главна парола." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Вписване със секретен ключ?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "Нямате елемент за вписване, подходящ за този уеб сайт." }, + "noMatchingLoginsForSite": { + "message": "Няма записи за вписване отговарящи на този уеб сайт" + }, "confirm": { "message": "Потвърждаване" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Запазване на секретния ключ като нов елемент за вписване" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Изберете елемент, в който да запазите този секретен ключ" }, + "chooseCipherForPasskeyAuth": { + "message": "Изберете секретен ключ, с който да се впишете" + }, "passkeyItem": { "message": "Секретен ключ" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,6 +3659,30 @@ "message": "Често използвани формати", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Продължаване към настройките на браузъра?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Продължаване към помощния център?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" + }, + "confirmContinueToHelpCenterPasswordManagementContent": { + "message": "Променете настройките на браузъра си за автоматично попълване и управление на паролите.", + "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": "Можете да прегледате и настроите клавишните комбинации за добавката в настройките на браузъра си.", + "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": "Променете настройките на браузъра си за автоматично попълване и управление на паролите.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" + }, + "confirmContinueToBrowserKeyboardShortcutSettingsContent": { + "message": "Можете да прегледате и настроите клавишните комбинации за добавката в настройките на браузъра си.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Искате ли да направите Битуорден своя управител на пароли по подразбиране?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" @@ -3317,10 +3711,18 @@ "message": "Идентификационните данни са запазени успешно!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Паролата е запазена!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Идентификационните данни са променени успешно!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Паролата е обновена!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Грешка при запазването на идентификационните данни. Вижте конзолата за подробности.", "description": "Notification message for when saving credentials has failed." @@ -3334,20 +3736,6 @@ "passkeyRemoved": { "message": "Секретният ключ е премахнат" }, - "unassignedItemsBannerNotice": { - "message": "Известие: неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори, а са достъпни само през Административната конзола." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Известие: след 16 май 2024, неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори, а ще бъдат достъпни само през Административната конзола." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Добавете тези елементи към колекция в", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "за да ги направите видими.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Автоматично попълване на предложения" }, @@ -3415,7 +3803,7 @@ }, "autofillTitle": { "message": "Авт. попълване – $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "Няма стойности за копиране" }, - "assignCollections": { - "message": "Свързване на колекции" + "assignToCollections": { + "message": "Свързване с колекции" }, "copyEmail": { "message": "Копиране на е-пощата" @@ -3493,13 +3881,13 @@ "message": "Елементи без папка" }, "itemDetails": { - "message": "Item details" + "message": "Подробности за елемента" }, "itemName": { - "message": "Item name" + "message": "Име на елемента" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Не можете да премахвате колекции с права „Само за преглед“: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3899,33 @@ "message": "Организацията е деактивирана" }, "owner": { - "message": "Owner" + "message": "Собственик" }, "selfOwnershipLabel": { - "message": "You", + "message": "Вие", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Записите в деактивирани организации не са достъпни. Свържете се със собственика на организацията си за помощ." }, + "additionalInformation": { + "message": "Допълнителна информация" + }, + "itemHistory": { + "message": "История на елемента" + }, + "lastEdited": { + "message": "Последна промяна" + }, + "ownerYou": { + "message": "Собственик: Вие" + }, + "linked": { + "message": "Свързано" + }, + "copySuccessful": { + "message": "Копирането е успешно" + }, "upload": { "message": "Качване" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Филтри" + }, + "personalDetails": { + "message": "Лични данни" + }, + "identification": { + "message": "Идентификация" + }, + "contactInfo": { + "message": "Информация за връзка" + }, + "downloadAttachment": { + "message": "Сваляне – $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "номерът на картата завършва с", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Данни за вписване" + }, + "authenticatorKey": { + "message": "Ключ за удостоверяване" + }, + "autofillOptions": { + "message": "Настройки за автоматичното попълване" + }, + "websiteUri": { + "message": "Уеб сайт (адрес)" + }, + "websiteUriCount": { + "message": "Уеб сайт (адрес) $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": "Уеб сайтът е добавен" + }, + "addWebsite": { + "message": "Добавяне на уеб сайт" + }, + "deleteWebsite": { + "message": "Изтриване на уеб сайт" + }, + "defaultLabel": { + "message": "По подразбиране ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Показване на откритото съвпадение $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Скриване на откритото съвпадение $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Автоматично попълване при зареждане на страницата?" + }, + "cardExpiredTitle": { + "message": "Изтекла карта" + }, + "cardExpiredMessage": { + "message": "Ако сте я подновили, актуализирайте информацията за картата" + }, + "cardDetails": { + "message": "Данни за картата" + }, + "cardBrandDetails": { + "message": "Подробности за $BRAND$", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Включване на анимациите" + }, + "addAccount": { + "message": "Добавяне на регистрация" + }, + "loading": { + "message": "Зареждане" + }, + "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": "Свързване" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Само членовете на организацията, които имат достъп до тези колекции, ще могат да виждат елемента." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Само членовете на организацията, които имат достъп до тези колекции, ще могат да виждат елементите." + }, + "bulkCollectionAssignmentWarning": { + "message": "Избрали сте $TOTAL_COUNT$ елемента Не можете да промените $READONLY_COUNT$ от елементите, тъй като нямате право за редактиране.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Добавяне на поле" + }, + "add": { + "message": "Добавяне" + }, + "fieldType": { + "message": "Тип на полето" + }, + "fieldLabel": { + "message": "Етикет на полето" + }, + "textHelpText": { + "message": "Използвайте текстови полета за обикновени данни, например въпроси за сигурност" + }, + "hiddenHelpText": { + "message": "Използвайте скрити полета за чувствителни данни, например пароли" + }, + "checkBoxHelpText": { + "message": "Използвайте полета за отметки, ако искате да попълвате автоматично такива полета във формуляри, например такова за запомняне на е-пощата" + }, + "linkedHelpText": { + "message": "Използвайте свързано поле, когато имате проблеми с автоматичното попълване на конкретен уеб сайт." + }, + "linkedLabelHelpText": { + "message": "Въведете подробности за полето от атрибутите му в HTML – id, name, aria-label или placeholder." + }, + "editField": { + "message": "Редактиране на полето" + }, + "editFieldLabel": { + "message": "Редактиране на $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Изтриване на $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "Добавено: $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Преместване на $LABEL$. Използвайте стрелките, за да преместите елемента нагоре или надолу.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "Преместено нагоре: $LABEL$. Позиция $INDEX$ от $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Изберете колекции за свързване" + }, + "personalItemTransferWarningSingular": { + "message": "1 елемент ще бъде преместен завинаги в избраната организация. Вече няма да притежавате този елемент." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ елемента ще бъдат преместени завинаги в избраната организация. Вече няма да притежавате тези елементи.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 елемент ще бъде преместен завинаги в $ORG$. Вече няма да притежавате този елемент.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ елемента ще бъдат преместени завинаги в $ORG$. Вече няма да притежавате тези елементи.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Успешно свързване на колекциите" + }, + "nothingSelected": { + "message": "Не сте избрали нищо." + }, + "movedItemsToOrg": { + "message": "Избраните записи бяха преместени в $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Елементите са преместени в $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Елементът е преместен в $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "Преместено надолу: $LABEL$. Позиция $INDEX$ от $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Местоположение на елемента" + }, + "fileSends": { + "message": "Файлови изпращания" + }, + "textSends": { + "message": "Текстови изпращания" + }, + "bitwardenNewLook": { + "message": "Биуорден има нов облик!" + }, + "bitwardenNewLookDesc": { + "message": "Сега е по-лесно и интуитивно от всякога да използвате автоматичното попълване и да търсите в раздела на трезора. Разгледайте!" + }, + "accountActions": { + "message": "Действия по регистрацията" + }, + "showNumberOfAutofillSuggestions": { + "message": "Показване на броя предложения за автоматично попълване на данни за вписване върху иконката на добавката" + }, + "systemDefault": { + "message": "По подразбиране за системата" + }, + "enterprisePolicyRequirementsApplied": { + "message": "Изискванията на политиката за големи компании бяха приложени към тази настройка" + }, + "fileSavedToDevice": { + "message": "Файлът е запазен на устройството. Можете да го намерите в мястото за сваляния на устройството." + }, + "showCharacterCount": { + "message": "Показване на броя знаци" + }, + "hideCharacterCount": { + "message": "Скриване на броя знаци" + }, + "itemsInTrash": { + "message": "Елементи в кошчето" + }, + "noItemsInTrash": { + "message": "Няма елементи в кошчето" + }, + "noItemsInTrashDesc": { + "message": "Елементите, които изтривате, ще бъдат премествани тук и изтривани окончателно след 30 дни" + }, + "trashWarning": { + "message": "Елементите, които са били в кошчето за повече от 30 дни, ще бъдат изтривани автоматично" + }, + "restore": { + "message": "Възстановяване" + }, + "deleteForever": { + "message": "Изтриване завинаги" + }, + "noEditPermissions": { + "message": "Нямате право за редактиране на този елемент" } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 3b79d49ceab..53a3ceb1a65 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "আপনার সুরক্ষিত ভল্টে প্রবেশ করতে লগ ইন করুন অথবা একটি নতুন অ্যাকাউন্ট তৈরি করুন।" }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "অ্যাকাউন্ট তৈরি করুন" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "প্রবেশ করুন" - }, "enterpriseSingleSignOn": { "message": "এন্টারপ্রাইজ একক সাইন-অন" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "মূল পাসওয়ার্ড ইঙ্গিত (ঐচ্ছিক)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "ট্যাব" }, @@ -107,17 +113,32 @@ "copySecurityCode": { "message": "সুরক্ষা কোড অনুলিপিত করুন" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "স্বতঃপূরণ" }, "autoFillLogin": { - "message": "Auto-fill login" + "message": "Autofill login" }, "autoFillCard": { - "message": "Auto-fill card" + "message": "Autofill card" }, "autoFillIdentity": { - "message": "Auto-fill identity" + "message": "Autofill identity" }, "generatePasswordCopied": { "message": "পাসওয়ার্ড তৈরি করুন (অনুলিপিকৃত)" @@ -280,6 +301,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,16 +384,56 @@ "message": "Minimum password length" }, "uppercase": { - "message": "Uppercase (A-Z)" + "message": "Uppercase (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Lowercase (a-z)" + "message": "Lowercase (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numbers (0-9)" + "message": "Numbers (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Special characters (!@#$%^&*)" + "message": "Special characters (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "শব্দের সংখ্যা" @@ -376,7 +455,12 @@ "message": "ন্যূনতম বিশেষ" }, "avoidAmbChar": { - "message": "অস্পষ্ট বর্ণগুলি এড়িয়ে চলুন" + "message": "অস্পষ্ট বর্ণগুলি এড়িয়ে চলুন", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "ভল্ট খুঁজুন" @@ -556,6 +640,18 @@ "security": { "message": "নিরাপত্তা" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "একটি ত্রুটি উৎপন্ন হয়েছে" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "আপনার নতুন অ্যাকাউন্ট তৈরি করা হয়েছে! আপনি এখন প্রবেশ করতে পারেন।" }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "যাচাইকরণ কোড প্রয়োজন।" }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "আপনার লগইন মাত্রকালটির মেয়াদ শেষ হয়ে গেছে।" }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "আপনি লগ আউট করতে চান?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "নতুন URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "বস্তু যোগ করা হয়েছে" }, @@ -737,23 +873,32 @@ "enableAddLoginNotification": { "message": "Ask to add login" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "\"লগইন যোগ করুন বিজ্ঞপ্তি\" স্বয়ংক্রিয়ভাবে আপনই যখনই প্রথমবারের জন্য লগ ইন করেন তখন আপনার ভল্টে নতুন লগইনগুলি সংরক্ষণ করতে অনুরোধ জানায়।" }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Show cards on Tab page" }, "showCardsCurrentTabDesc": { - "message": "List card items on the Tab page for easy auto-fill." + "message": "List card items on the Tab page for easy autofill." + }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" }, "showIdentitiesCurrentTabDesc": { - "message": "List identity items on the Tab page for easy auto-fill." + "message": "List identity items on the Tab page for easy autofill." }, "clearClipboard": { "message": "ক্লিপবোর্ড পরিষ্কার", @@ -791,7 +936,7 @@ "message": "হ্যাঁ, এখনই হালনাগাদ করুন" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Unlock" @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "পূর্ব-নির্ধারিত URI মিল সনাক্তকরণ", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "স্বতঃপূরণের মতো ক্রিয়া সম্পাদন করার সময় লগইনগুলির জন্য URI মিল সনাক্তকরণ যে পূর্ব-নির্ধারিত পদ্ধতিতে পরিচালনা করা হবে তা চয়ন করুন।" @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "ফাইল সংযুক্তির জন্য ১ জিবি এনক্রিপ্টেড স্থান।" }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "আপনি bitwarden.com ওয়েব ভল্টে প্রিমিয়াম সদস্যতা কিনতে পারেন। আপনি কি এখনই ওয়েবসাইটটি দেখতে চান?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "আপনি প্রিমিয়াম সদস্য!" }, "premiumCurrentMemberThanks": { "message": "Bitwarden কে সমর্থন করার জন্য আপনাকে ধন্যবাদ।" }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "সমস্ত মাত্র $PRICE$ / বছরের জন্য!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "পুনঃসতেজ সম্পূর্ণ" }, @@ -1178,14 +1341,23 @@ "message": "পরিবেশের URL গুলি সংরক্ষণ করা হয়েছে।" }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,38 +1371,57 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "পৃষ্ঠা লোডে স্বতঃপূরণ সক্ষম করুন" }, "enableAutoFillOnPageLoadDesc": { "message": "যদি কোনও লগইন ফর্ম সনাক্ত হয়, ওয়েব পৃষ্ঠাটি লোড হওয়ার পরে স্বয়ংক্রিয়ভাবে স্বতঃপূরণ করুন।" }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Compromised or untrusted websites can exploit autofill on page load." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" }, "learnMoreAboutAutofill": { - "message": "Learn more about auto-fill" + "message": "Learn more about autofill" }, "defaultAutoFillOnPageLoad": { "message": "Default autofill setting for login items" }, "defaultAutoFillOnPageLoadDesc": { - "message": "You can turn off auto-fill on page load for individual login items from the item's Edit view." + "message": "You can turn off autofill on page load for individual login items from the item's Edit view." }, "itemAutoFillOnPageLoad": { - "message": "Auto-fill on page load (if set up in Options)" + "message": "Autofill on page load (if set up in Options)" }, "autoFillOnPageLoadUseDefault": { "message": "Use default setting" }, "autoFillOnPageLoadYes": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "autoFillOnPageLoadNo": { - "message": "Do not auto-fill on page load" + "message": "Do not autofill on page load" }, "commandOpenPopup": { "message": "ভল্ট পপআপ খুলুন" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "সাইডবারে ভল্ট খুলুন" }, - "commandAutofillDesc": { - "message": "বর্তমান ওয়েবসাইটটির জন্য সর্বশেষ ব্যবহৃত লগইনটি স্বতঃপূরণ করুন" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "ক্লিপবোর্ডে একটি নতুন এলোমেলো পাসওয়ার্ড তৈরি এবং অনুলিপিত করুন" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "বুলিয়ান" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Linked", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "পাসওয়ার্ড ইতিহাস" }, @@ -1533,6 +1742,10 @@ "message": "ভিত্তি ডোমেইন", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domain name", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "মিল সনাক্তকরণ", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "পূর্ব-নির্ধারিত মিল সনাক্তকরণ", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "বিকল্পগুলি টগল করুন" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "তালিকার জন্য কোনও পাসওয়ার্ড নেই।" }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "সরান" }, @@ -1677,6 +1899,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": "ভল্টের সময়সীমা কর্ম" }, @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "আপনার নতুন মূল পাসওয়ার্ড নীতির প্রয়োজনীয়তা পূরণ করে না।" }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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": "বায়োমেট্রিকস সক্ষম নেই" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ is not a valid domain", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "পাসওয়ার্ড সুরক্ষিত" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Send লিঙ্ক কপি করুন", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send created", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "ইমেইল সত্যায়ন প্রয়োজন" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2606,10 +2906,10 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { - "message": "How to auto-fill" + "message": "How to autofill" }, "autofillSelectInfoWithCommand": { "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", @@ -2627,16 +2927,25 @@ "message": "Got it" }, "autofillSettings": { - "message": "Auto-fill settings" + "message": "Autofill settings" + }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 11e393476f2..13cd7e7f54a 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Prijavite se ili napravite novi račun da biste pristupili svom sigurnom trezoru." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "Napravi račun" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "Prijavite se" - }, "enterpriseSingleSignOn": { "message": "Enterprise single sign-on" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Master password hint (optional)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "Tab" }, @@ -107,17 +113,32 @@ "copySecurityCode": { "message": "Copy security code" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { - "message": "Auto-fill" + "message": "Autofill" }, "autoFillLogin": { - "message": "Auto-fill login" + "message": "Autofill login" }, "autoFillCard": { - "message": "Auto-fill card" + "message": "Autofill card" }, "autoFillIdentity": { - "message": "Auto-fill identity" + "message": "Autofill identity" }, "generatePasswordCopied": { "message": "Generate password (copied)" @@ -150,7 +171,7 @@ "message": "Log in to your vault" }, "autoFillInfo": { - "message": "There are no logins available to auto-fill for the current browser tab." + "message": "There are no logins available to autofill for the current browser tab." }, "addLogin": { "message": "Add a login" @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "Minimum password length" }, "uppercase": { - "message": "Uppercase (A-Z)" + "message": "Uppercase (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Lowercase (a-z)" + "message": "Lowercase (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numbers (0-9)" + "message": "Numbers (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Special characters (!@#$%^&*)" + "message": "Special characters (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Number of words" @@ -376,7 +455,12 @@ "message": "Minimum special" }, "avoidAmbChar": { - "message": "Avoid ambiguous characters" + "message": "Avoid ambiguous characters", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Search vault" @@ -556,6 +640,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -613,7 +718,7 @@ } }, "autofillError": { - "message": "Unable to auto-fill the selected item on this page. Copy and paste the information instead." + "message": "Unable to autofill the selected item on this page. Copy and paste the information instead." }, "totpCaptureError": { "message": "Unable to scan QR code from the current webpage" @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Your login session has expired." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "New URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "Item added" }, @@ -737,23 +873,32 @@ "enableAddLoginNotification": { "message": "Ask to add login" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "Ask to add an item if one isn't found in your vault." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Show cards on Tab page" }, "showCardsCurrentTabDesc": { - "message": "List card items on the Tab page for easy auto-fill." + "message": "List card items on the Tab page for easy autofill." + }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" }, "showIdentitiesCurrentTabDesc": { - "message": "List identity items on the Tab page for easy auto-fill." + "message": "List identity items on the Tab page for easy autofill." }, "clearClipboard": { "message": "Clear clipboard", @@ -791,7 +936,7 @@ "message": "Update" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Unlock" @@ -810,10 +955,10 @@ }, "defaultUriMatchDetection": { "message": "Default URI match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { - "message": "Choose the default way that URI match detection is handled for logins when performing actions such as auto-fill." + "message": "Choose the default way that URI match detection is handled for logins when performing actions such as autofill." }, "theme": { "message": "Theme" @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a Premium member!" }, "premiumCurrentMemberThanks": { "message": "Thank you for supporting Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "All for just $PRICE$ /year!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Refresh complete" }, @@ -1028,7 +1191,7 @@ "message": "Copy TOTP automatically" }, "disableAutoTotpCopyDesc": { - "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login." + "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you autofill the login." }, "enableAutoBiometricsPrompt": { "message": "Ask for biometrics on launch" @@ -1178,14 +1341,23 @@ "message": "Environment URLs saved" }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,38 +1371,57 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "enableAutoFillOnPageLoadDesc": { - "message": "If a login form is detected, auto-fill when the web page loads." + "message": "If a login form is detected, autofill when the web page loads." + }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Compromised or untrusted websites can exploit autofill on page load." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" }, "learnMoreAboutAutofill": { - "message": "Learn more about auto-fill" + "message": "Learn more about autofill" }, "defaultAutoFillOnPageLoad": { "message": "Default autofill setting for login items" }, "defaultAutoFillOnPageLoadDesc": { - "message": "You can turn off auto-fill on page load for individual login items from the item's Edit view." + "message": "You can turn off autofill on page load for individual login items from the item's Edit view." }, "itemAutoFillOnPageLoad": { - "message": "Auto-fill on page load (if set up in Options)" + "message": "Autofill on page load (if set up in Options)" }, "autoFillOnPageLoadUseDefault": { "message": "Use default setting" }, "autoFillOnPageLoadYes": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "autoFillOnPageLoadNo": { - "message": "Do not auto-fill on page load" + "message": "Do not autofill on page load" }, "commandOpenPopup": { "message": "Open vault popup" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Open vault in sidebar" }, - "commandAutofillDesc": { - "message": "Auto-fill the last used login for the current website" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Generate and copy a new random password to the clipboard" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Linked", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -1533,6 +1742,10 @@ "message": "Base domain", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domain name", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Match detection", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Default match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Toggle options" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "There are no passwords to list." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Remove" }, @@ -1677,6 +1899,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" }, @@ -1716,16 +1961,16 @@ "message": "Timeout action confirmation" }, "autoFillAndSave": { - "message": "Auto-fill and save" + "message": "Autofill and save" }, "fillAndSave": { "message": "Fill and save" }, "autoFillSuccessAndSavedUri": { - "message": "Item auto-filled and URI saved" + "message": "Item autofilled and URI saved" }, "autoFillSuccess": { - "message": "Item auto-filled " + "message": "Item autofilled " }, "insecurePageWarning": { "message": "Warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page." @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ is not a valid domain", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Password protected" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Copy Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send created", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2606,10 +2906,10 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { - "message": "How to auto-fill" + "message": "How to autofill" }, "autofillSelectInfoWithCommand": { "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", @@ -2627,16 +2927,25 @@ "message": "Got it" }, "autofillSettings": { - "message": "Auto-fill settings" + "message": "Autofill settings" + }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 5029057167e..160b625c4e3 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Inicieu sessió o creeu un compte nou per accedir a la caixa forta." }, + "inviteAccepted": { + "message": "Invitació acceptada" + }, "createAccount": { "message": "Crea un compte" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Acabeu de crear el vostre compte establint una contrasenya" }, - "login": { - "message": "Inicia sessió" - }, "enterpriseSingleSignOn": { "message": "Inici de sessió únic d'empresa" }, @@ -50,7 +50,7 @@ "message": "Una pista de contrasenya mestra us pot ajudar a recordar-la si l'oblideu." }, "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", @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Pista de la contrasenya mestra (opcional)" }, + "joinOrganization": { + "message": "Uneix-te a l'organització" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Acabeu d'unir-vos a aquesta organització establint una contrasenya mestra." + }, "tab": { "message": "Pestanya" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Copia el codi de seguretat" }, + "copyName": { + "message": "Copia el nom" + }, + "copyCompany": { + "message": "Copia la companyia" + }, + "copySSN": { + "message": "Copia número de la Seguretat Social" + }, + "copyPassportNumber": { + "message": "Copia el número de passaport" + }, + "copyLicenseNumber": { + "message": "Copia el número de llicència" + }, "autoFill": { "message": "Emplenament automàtic" }, @@ -192,19 +213,19 @@ "message": "Continua cap a l'aplicació web?" }, "continueToWebAppDesc": { - "message": "Explore more features of your Bitwarden account on the web app." + "message": "Exploreu més característiques del vostre compte Bitwarden a l'aplicació web." }, "continueToHelpCenter": { - "message": "Continue to Help Center?" + "message": "Voleu continuar cap al Centre d'ajuda?" }, "continueToHelpCenterDesc": { - "message": "Learn more about how to use Bitwarden on the Help Center." + "message": "Obteniu més informació sobre com utilitzar Bitwarden al Centre d'ajuda." }, "continueToBrowserExtensionStore": { - "message": "Continue to browser extension store?" + "message": "Voleu continuar cap a la botiga d'extensions del navegador?" }, "continueToBrowserExtensionStoreDesc": { - "message": "Help others find out if Bitwarden is right for them. Visit your browser's extension store and leave a rating now." + "message": "Ajudeu els altres a descobrir si Bitwarden és adequat per a ells. Visiteu la botiga d'aplicacions i deixeu-nos una valoració ara." }, "changeMasterPasswordOnWebConfirmation": { "message": "Podeu canviar la vostra contrasenya mestra a l'aplicació web de Bitwarden." @@ -224,43 +245,43 @@ "message": "Tanca la sessió" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "Quant a Bitwarden" }, "about": { "message": "Quant a" }, "moreFromBitwarden": { - "message": "More from Bitwarden" + "message": "Més de Bitwarden" }, "continueToBitwardenDotCom": { - "message": "Continue to bitwarden.com?" + "message": "Voleu continuar cap a bitwarden.com?" }, "bitwardenForBusiness": { - "message": "Bitwarden for Business" + "message": "Bitwarden per a negocis" }, "bitwardenAuthenticator": { - "message": "Bitwarden Authenticator" + "message": "Autenticador 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": "L'autenticador Bitwarden us permet emmagatzemar claus d'autenticació i generar codis TOTP per a fluxos de verificació en dos passos. Més informació al lloc web bitwarden.com" }, "bitwardenSecretsManager": { - "message": "Bitwarden Secrets Manager" + "message": "Administrador de secrets de Bitwarden" }, "continueToSecretsManagerPageDesc": { - "message": "Securely store, manage, and share developer secrets with Bitwarden Secrets Manager. Learn more on the bitwarden.com website." + "message": "Emmagatzema, gestiona i comparteix de manera segura els secrets dels desenvolupadors amb l'Administrador de secrets de Bitwarden. Més informació al lloc web 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": "Creeu experiències d'inici de sessió segures i sense contrasenyes tradicionals amb Passwordless.dev. Més informació al lloc web bitwarden.com." }, "freeBitwardenFamilies": { - "message": "Free Bitwarden Families" + "message": "Famílies Bitwarden gratuït" }, "freeBitwardenFamiliesPageDesc": { - "message": "You are eligible for Free Bitwarden Families. Redeem this offer today in the web app." + "message": "Sou elegible per a Famílies Bitwarden gratuït. Bescanvia aquesta oferta hui a l'aplicació web." }, "version": { "message": "Versió" @@ -280,6 +301,24 @@ "editFolder": { "message": "Edita la carpeta" }, + "newFolder": { + "message": "Carpeta nova" + }, + "folderName": { + "message": "Nom de la carpeta" + }, + "folderHintText": { + "message": "Imbriqueu una carpeta afegint el nom de la carpeta principal seguit d'una \"/\". Exemple: Social/Fòrums" + }, + "noFoldersAdded": { + "message": "No s'ha afegit cap carpeta" + }, + "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" }, @@ -321,7 +360,7 @@ "message": "Genera automàticament contrasenyes fortes i úniques per als vostres inicis de sessió." }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "Aplicació web Bitwarden" }, "importItems": { "message": "Importa elements" @@ -345,16 +384,56 @@ "message": "Longitud mínima de la contrasenya" }, "uppercase": { - "message": "Majúscula (A-Z)" + "message": "Majúscula (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Minúscula (a-z)" + "message": "Minúscula (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Números (0-9)" + "message": "Números (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Caràcters especials (!@#$%^&*)" + "message": "Caràcters especials (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Nombre de paraules" @@ -376,7 +455,12 @@ "message": "Mínim de caràcters especials" }, "avoidAmbChar": { - "message": "Eviteu caràcters ambigus" + "message": "Eviteu caràcters ambigus", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Cerca en la caixa forta" @@ -409,13 +493,13 @@ "message": "Preferit" }, "unfavorite": { - "message": "Unfavorite" + "message": "Trau dels preferits" }, "itemAddedToFavorites": { - "message": "Item added to favorites" + "message": "Element afegit als preferits" }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "Element suprimit dels preferits" }, "notes": { "message": "Notes" @@ -439,7 +523,7 @@ "message": "Inicia" }, "launchWebsite": { - "message": "Launch website" + "message": "Obri la web" }, "website": { "message": "Lloc web" @@ -556,6 +640,18 @@ "security": { "message": "Seguretat" }, + "confirmMasterPassword": { + "message": "Confirma la contrasenya mestra" + }, + "masterPassword": { + "message": "Contrasenya mestra" + }, + "masterPassImportant": { + "message": "La contrasenya mestra no es pot recuperar si la oblideu!" + }, + "masterPassHintLabel": { + "message": "Pista de la contrasenya mestra" + }, "errorOccurred": { "message": "S'ha produït un error" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "El vostre compte s'ha creat correctament. Ara ja podeu iniciar sessió." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "Heu iniciat sessió correctament" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "El codi de verificació és obligatori." }, + "webauthnCancelOrTimeout": { + "message": "L'autenticació s'ha cancel·lat o ha tardat massa. Torna-ho a provar." + }, "invalidVerificationCode": { "message": "Codi de verificació no vàlid" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Escaneja el codi QR de l'autenticador des de la pàgina web actual" }, + "totpHelperTitle": { + "message": "Feu que la verificació en dos passos siga perfecta" + }, + "totpHelper": { + "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." + }, + "learnMoreAboutAuthenticators": { + "message": "Learn more about authenticators" + }, "copyTOTP": { "message": "Copia la clau de l'autenticador (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "La vostra sessió ha caducat." }, + "logIn": { + "message": "Inicia sessió" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Segur que voleu tancar la sessió?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Nova URI" }, + "addDomain": { + "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": { "message": "Element afegit" }, @@ -737,23 +873,32 @@ "enableAddLoginNotification": { "message": "Demana d'afegir els inicis de sessió" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "La \"Notificació per afegir inicis de sessió\" demana automàticament que guardeu els nous inicis de sessió a la vostra caixa forta quan inicieu la sessió per primera vegada." }, "addLoginNotificationDescAlt": { "message": "Demana afegir un element si no se'n troba cap a la caixa forta. S'aplica a tots els comptes connectats." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Mostra les targetes a la pàgina de pestanya" }, "showCardsCurrentTabDesc": { "message": "Llista els elements de la targeta a la pàgina de pestanya per facilitar l'autoemplenat." }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" + }, "showIdentitiesCurrentTab": { "message": "Mostra les identitats a la pàgina de pestanya" }, "showIdentitiesCurrentTabDesc": { - "message": "Llista els elements d'identitat de la pàgina de pestanya per facilitar l'autoemplenat." + "message": "Llista els elements d'identitat de la pestanya de la pàgina per facilitar l'autoemplenat." }, "clearClipboard": { "message": "Buida el porta-retalls", @@ -797,7 +942,7 @@ "message": "Desbloqueja" }, "additionalOptions": { - "message": "Additional options" + "message": "Opcions addicionals" }, "enableContextMenuItem": { "message": "Mostra les opcions del menú contextual" @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Detecció de coincidències URI per defecte", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Trieu la manera predeterminada en que es gestiona la detecció de coincidència d'URI per als inicis de sessió en realitzar accions com l'emplenament automàtic." @@ -837,7 +982,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" @@ -849,25 +994,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", @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB d'emmagatzematge xifrat per als fitxers adjunts." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Opcions propietàries de doble factor com ara YubiKey i Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Podeu comprar la vostra subscripció a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Sou un membre premium!" }, "premiumCurrentMemberThanks": { "message": "Gràcies per donar suport a Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "Tot per només $PRICE$ / any!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Actualització completa" }, @@ -1179,10 +1342,19 @@ }, "showAutoFillMenuOnFormFields": { "message": "Mostra el menú d'emplenament automàtic als camps del formulari", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { - "message": "S'aplica a tots els comptes connectats." + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { + "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { "message": "Desactiveu la configuració integrada del gestor de contrasenyes del vostre navegador per evitar conflictes." @@ -1202,15 +1374,34 @@ "message": "Quan la icona d'emplenament automàtic està seleccionada", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "Habilita l'emplenament automàtic en carregar la pàgina" }, "enableAutoFillOnPageLoadDesc": { "message": "Si es detecta un formulari d'inici de sessió, es realitza automàticament un emplenament automàtic quan es carrega la pàgina web." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Els llocs web compromesos o no fiables poden aprofitar-se de l'emplenament automàtic en carregar de la pàgina." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" + }, "learnMoreAboutAutofill": { "message": "Obteniu més informació sobre l'emplenament automàtic" }, @@ -1218,7 +1409,7 @@ "message": "Configuració per defecte d'emplenament automàtic per als elements d'inici de sessió" }, "defaultAutoFillOnPageLoadDesc": { - "message": "Després d'habilitar l'emplenament automàtic a la càrrega de la pàgina, podeu habilitar o inhabilitar la característica per a elements d'inici de sessió individuals. Aquesta és la configuració per defecte per als elements d'inici de sessió que no estan configurats per separat." + "message": "Podeu desactivar l'emplenament automàtic a la càrrega de la pàgina per a elements d'inici de sessió individuals des de la vista d'edició de l'element." }, "itemAutoFillOnPageLoad": { "message": "Emplenament automàtic a la càrrega de la pàgina (si està habilitat a Opcions)" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Obri la caixa forta a la barra lateral" }, - "commandAutofillDesc": { - "message": "Ompliu automàticament amb l'últim accés utilitzat per al lloc web actual." + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Genera i copia una nova contrasenya aleatòria al porta-retalls." @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Booleà" }, + "cfTypeCheckbox": { + "message": "Casella de selecció" + }, "cfTypeLinked": { "message": "Enllaçat", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Historial de les contrasenyes" }, @@ -1533,6 +1742,10 @@ "message": "Domini base", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Nom del domini", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Detecció de coincidències", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Detecció de coincidències per defecte", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Commuta opcions" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "No hi ha cap contrasenya a llistar." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Suprimeix" }, @@ -1677,6 +1899,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" }, @@ -1799,11 +2044,11 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "La nova contrasenya principal no compleix els requisits de la política." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "Anul·la subscripció" }, "atAnyTime": { "message": "at any time." @@ -1812,7 +2057,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:" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "La biometria ha fallat" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "Una política d'organització ha bloquejat la importació d'elements a la vostra caixa forta individual." }, + "domainsTitle": { + "message": "Dominis", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Dominis exclosos" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden no demanarà que es guarden les dades d'inici de sessió d'aquests dominis per a tots els comptes iniciats. Heu d'actualitzar la pàgina perquè els canvis tinguen efecte." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ no és un domini vàlid", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Protegit amb contrasenya" }, + "copyLink": { + "message": "Copia l'enllaç" + }, "copySendLink": { "message": "Copia l'enllaç Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send creat", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send guardat", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Es requereix verificació del correu electrònic" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Heu de verificar el correu electrònic per utilitzar aquesta característica. Podeu verificar el vostre correu electrònic a la caixa forta web." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Inscripció automàtica" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Configuració d'emplenament automàtic" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" + }, "autofillShortcut": { "message": "Drecera de teclat d'emplenament automàtic" }, - "autofillShortcutNotSet": { - "message": "La drecera d'emplenament automàtic no està configurada. Canvieu-ho a la configuració del navegador." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "La drecera d'emplenament automàtic és: $COMMAND$. Canvieu-ho a la configuració del navegador.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2696,7 +3005,7 @@ "message": "No email?" }, "goBack": { - "message": "Go back" + "message": "Torna arrere" }, "toEditYourEmailAddress": { "message": "to edit your email address." @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Dispositiu de confiança" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "L'entrada és obligatòria." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Selecciona --" }, @@ -2878,12 +3207,12 @@ "message": "Alies de domini" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Els elements amb una nova sol·licitud de contrasenya mestra no es poden omplir automàticament en carregar la pàgina. L'emplenament automàtic en carregar de la pàgina està desactivat.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Els elements amb una nova sol·licitud de contrasenya mestra no es poden omplir automàticament en carregar la pàgina. L'emplenament automàtic en carregar la pàgina està desactivat.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "S'ha configurat l'emplenament automàtic en carregar la pàgina per que utilitze la configuració predeterminada.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "S'ha configurat l'emplenament automàtic en carregar la pàgina perquè utilitze la configuració predeterminada.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Desactiveu la sol·licitud de nova contrasenya mestra per editar aquest camp", @@ -2911,10 +3240,18 @@ "message": "Desbloqueja el compte per veure els inicis de sessió coincidents", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Desbloqueja el compte", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Ompliu les credencials per a", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Afegeix un nou element a la caixa forta", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "Nova targeta", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "El menú d'emplenament automàtic de Bitwarden està disponible. Premeu la tecla de fletxa avall per seleccionar-lo.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Inicieu DUO i seguiu els passos per finalitzar la sessió." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "La contrasenya del fitxer no és vàlida. Utilitzeu la contrasenya que vau introduir quan vau crear el fitxer d'exportació." }, - "importDestination": { - "message": "Destinació de la importació" + "destination": { + "message": "Destinació" }, "learnAboutImportOptions": { "message": "Obteniu informació sobre les opcions d'importació" @@ -3108,7 +3472,7 @@ "message": "Confirma la contrasenya del fitxer" }, "exportSuccess": { - "message": "Vault data exported" + "message": "S'han exportat les dades de la caixa forta" }, "typePasskey": { "message": "Clau de pas" @@ -3122,8 +3486,8 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verificació requerida pel lloc iniciador. Aquesta funció encara no s'ha implementat per als comptes sense contrasenya mestra." }, - "logInWithPasskey": { - "message": "Inici de sessió amb clau de pas?" + "logInWithPasskeyQuestion": { + "message": "Log in with passkey?" }, "passkeyAlreadyExists": { "message": "Ja hi ha una clau de pas per a aquesta aplicació." @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "No teniu cap inici de sessió que coincidisca amb el d'aquest lloc." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirma-ho" }, @@ -3143,8 +3510,11 @@ "savePasskeyNewLogin": { "message": "Guarda la clau de pas com a nou inici de sessió" }, - "choosePasskey": { - "message": "Trieu un inici de sessió per guardar aquesta clau de pas" + "chooseCipherForPasskeySave": { + "message": "Choose a login to save this passkey to" + }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" }, "passkeyItem": { "message": "Element de clau de pas" @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,6 +3659,30 @@ "message": "Formats comuns", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Fer que Bitwarden siga el gestor de contrasenyes predeterminat?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" @@ -3317,10 +3711,18 @@ "message": "Les credencials s'han guardat correctament!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Les credencials s'han actualitzat correctament!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "S'ha produït un error en guardar les credencials. Consulteu la consola per obtenir més informació.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Clau de pas suprimida" }, - "unassignedItemsBannerNotice": { - "message": "Avís: els elements de l'organització no assignats ja no són visibles a la visualització de Totes les caixes fortes i només es poden accedir des de la Consola d'administració." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Avís: el 16 de maig de 2024, els elements de l'organització no assignats deixaran de ser visibles a la visualització de Totes les caixes fortes i només es podran accedir des de la Consola d'administració." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assigna aquests elements a una col·lecció de", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "per fer-los visibles.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3477,7 +3865,7 @@ } }, "new": { - "message": "New" + "message": "Nou" }, "removeItem": { "message": "Remove $NAME$", @@ -3493,10 +3881,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$", @@ -3511,14 +3899,32 @@ "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": "Informació addicional" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Última edició" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Enllaçat" + }, + "copySuccessful": { + "message": "Copy Successful" }, "upload": { "message": "Upload" @@ -3557,6 +3963,380 @@ "message": "Free organizations cannot use attachments" }, "filters": { - "message": "Filters" + "message": "Filtres" + }, + "personalDetails": { + "message": "Detalls personals" + }, + "identification": { + "message": "Identificació" + }, + "contactInfo": { + "message": "Informació de contacte" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Credencials d'inici de sessió" + }, + "authenticatorKey": { + "message": "Clau autenticadora" + }, + "autofillOptions": { + "message": "Opcions d'emplenament automàtic" + }, + "websiteUri": { + "message": "Lloc web (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": "Lloc web afegit" + }, + "addWebsite": { + "message": "Afig un lloc web" + }, + "deleteWebsite": { + "message": "Suprimeix lloc web" + }, + "defaultLabel": { + "message": "Per defecte ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Afig compte" + }, + "loading": { + "message": "S'està carregant" + }, + "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": "Assigna" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Afig un camp" + }, + "add": { + "message": "Afig" + }, + "fieldType": { + "message": "Tipus de camp" + }, + "fieldLabel": { + "message": "Etiqueta del camp" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edita el camp" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "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." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index e2b8ceb7df1..bb3c81e0785 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Pro přístup do Vašeho bezpečného trezoru se přihlaste nebo si vytvořte nový účet." }, + "inviteAccepted": { + "message": "Pozvánka byla přijata" + }, "createAccount": { "message": "Vytvořit účet" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Dokončete vytváření účtu nastavením hesla" }, - "login": { - "message": "Přihlásit se" - }, "enterpriseSingleSignOn": { "message": "Jednotné podnikové přihlášení" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Nápověda k hlavnímu heslu (volitelné)" }, + "joinOrganization": { + "message": "Přidat se k organizaci" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Dokončete připojení k této organizaci nastavením hlavního hesla." + }, "tab": { "message": "Karta" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Kopírovat bezpečnostní kód" }, + "copyName": { + "message": "Kopírovat název" + }, + "copyCompany": { + "message": "Kopírovat společnost" + }, + "copySSN": { + "message": "Kopírovat rodné číslo" + }, + "copyPassportNumber": { + "message": "Kopírovat číslo pasu" + }, + "copyLicenseNumber": { + "message": "Kopírovat číslo dokladu totožnosti" + }, "autoFill": { "message": "Automatické vyplňování" }, @@ -280,6 +301,24 @@ "editFolder": { "message": "Upravit složku" }, + "newFolder": { + "message": "Nová složka" + }, + "folderName": { + "message": "Název složky" + }, + "folderHintText": { + "message": "Vnořit složku přidáním názvu nadřazené složky následovaného znakem \"/\". Příklad: Sociální/Fóra" + }, + "noFoldersAdded": { + "message": "Nebyly přidány žádné složky" + }, + "createFoldersToOrganize": { + "message": "Vytvořte složky pro organizaci Vašich položek trezoru" + }, + "deleteFolderPermanently": { + "message": "Opravdu chcete trvale smazat tuto složku?" + }, "deleteFolder": { "message": "Smazat složku" }, @@ -345,16 +384,56 @@ "message": "Minimální délka hesla" }, "uppercase": { - "message": "Velká písmena (A-Z)" + "message": "Velká písmena (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Malá písmena (a-z)" + "message": "Malá písmena (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Číslice (0-9)" + "message": "Číslice (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Speciální znaky (!@#$%^&*)" + "message": "Speciální znaky (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Zahrnout", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Zahrnout velká písmena", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Zahrnout malá písmena", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Zahrnout číslice", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Zahrnout speciální znaky", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Počet slov" @@ -376,7 +455,12 @@ "message": "Minimální počet speciálních znaků" }, "avoidAmbChar": { - "message": "Nepoužívat zaměnitelné znaky" + "message": "Nepoužívat zaměnitelné znaky", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Nepoužívat zaměnitelné znaky", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Vyhledat v trezoru" @@ -556,6 +640,18 @@ "security": { "message": "Zabezpečení" }, + "confirmMasterPassword": { + "message": "Potvrzení hlavního hesla" + }, + "masterPassword": { + "message": "Hlavní heslo" + }, + "masterPassImportant": { + "message": "Pokud zapomenete Vaše hlavní heslo, nebude možné jej obnovit!" + }, + "masterPassHintLabel": { + "message": "Nápověda k hlavnímu heslu" + }, "errorOccurred": { "message": "Vyskytla se chyba" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Váš účet byl vytvořen! Můžete se přihlásit." }, + "newAccountCreated2": { + "message": "Váš nový účet byl vytvořen!" + }, + "youHaveBeenLoggedIn": { + "message": "Byli jste přihlášeni!" + }, "youSuccessfullyLoggedIn": { "message": "Byli jste úspěšně přihlášeni" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Je vyžadován ověřovací kód." }, + "webauthnCancelOrTimeout": { + "message": "Ověření bylo zrušeno nebo trvalo příliš dlouho. Zkuste to znovu." + }, "invalidVerificationCode": { "message": "Neplatný ověřovací kód" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Naskenovat QR kód z aktuální webové stránky" }, + "totpHelperTitle": { + "message": "Bezproblémové dvoufázové ověřování" + }, + "totpHelper": { + "message": "Bitwarden umí ukládat a vyplňovat dvoufázové ověřovací kódy. Zkopírujte a vložte klíč do tohoto pole." + }, + "totpHelperWithCapture": { + "message": "Bitwarden umí ukládat a vyplňovat dvoufázové ověřovací kódy. Vyberte ikonu fotoaparátu a pořiďte snímek obrazovky ověřovacího QR kódu této webové stránky nebo klíč zkopírujte a vložte do tohoto pole." + }, + "learnMoreAboutAuthenticators": { + "message": "Zjistěte více o autentizátorech" + }, "copyTOTP": { "message": "Kopírovat autentizační klíč (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Platnost přihlášení vypršela." }, + "logIn": { + "message": "Přihlásit se" + }, + "restartRegistration": { + "message": "Restartovat registraci" + }, + "expiredLink": { + "message": "Platnost odkazu vypršela" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Restartujte registraci nebo se zkuste přihlásit." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Už možná máte účet" + }, "logOutConfirmation": { "message": "Opravdu se chcete odhlásit?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Nová URI" }, + "addDomain": { + "message": "Přidat doménu", + "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": { "message": "Položka byla přidána" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Ptát se na přidání přihlášení" }, + "vaultSaveOptionsTitle": { + "message": "Uložit do voleb trezoru" + }, "addLoginNotificationDesc": { "message": "Zeptá se na uložení údajů, pokud nebyly v trezoru nalezeny." }, "addLoginNotificationDescAlt": { "message": "Požádá o přidání položky, pokud nebyla nalezena v trezoru. Platí pro všechny přihlášené účty." }, + "showCardsInVaultView": { + "message": "Zobrazit karty jako návrhy automatického vyplňování v zobrazení trezoru" + }, "showCardsCurrentTab": { "message": "Zobrazit platební karty na obrazovce Karta" }, "showCardsCurrentTabDesc": { "message": "Pro snadné vyplnění zobrazí platební karty na obrazovce Karta." }, + "showIdentitiesInVaultView": { + "message": "Zobrazit identity jako návrhy automatického vyplňování v zobrazení trezoru" + }, "showIdentitiesCurrentTab": { "message": "Zobrazit identity na obrazovce Karta" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Výchozí zjišťování shody URI", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Vyberte výchozí způsob, jakým se detekuje shoda URI přihlašovacích údajů. Používá se například pro automatické vyplňování." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB šifrovaného úložiště pro přílohy." }, + "premiumSignUpEmergency": { + "message": "Nouzový přístup" + }, "premiumSignUpTwoStepOptions": { "message": "Volby proprietálních dvoufázových přihlášení jako je YubiKey a Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Prémiové členství můžete zakoupit na webové stránce bitwarden.com. Chcete tuto stránku nyní otevřít?" }, + "premiumPurchaseAlertV2": { + "message": "Premium si můžete zakoupit v nastavení účtu ve webové aplikaci Bitwarden." + }, "premiumCurrentMember": { "message": "Jste prémiovým členem!" }, "premiumCurrentMemberThanks": { "message": "Děkujeme za podporu Bitwardenu." }, + "premiumFeatures": { + "message": "Přejděte na Premium a získáte:" + }, "premiumPrice": { "message": "Vše jen za $PRICE$ ročně!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "Vše jen za $PRICE$ ročně!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Obnova je dokončena" }, @@ -1179,9 +1342,18 @@ }, "showAutoFillMenuOnFormFields": { "message": "Zobrazit menu automatického vyplňování v polích formuláře", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Návrhy automatického vyplňování" + }, + "showInlineMenuLabel": { + "message": "Zobrazit návrhy automatického vyplňování v polích formuláře" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Zobrazit návrhy, když je vybrána ikona" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Použije se na všechny přihlášené účty." }, "turnOffBrowserBuiltInPasswordManagerSettings": { @@ -1202,15 +1374,34 @@ "message": "Když je vybrána ikona automatického vyplňování", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Automaticky vyplnit údaje při načtení stránky" + }, "enableAutoFillOnPageLoad": { "message": "Automaticky vyplnit údaje při načtení stránky" }, "enableAutoFillOnPageLoadDesc": { "message": "Pokud je zjištěn přihlašovací formulář, automaticky se při načítání webové stránky vyplní přihlašovací údaje." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Varování:$CLOSETAG$ Kompromitované nebo nedůvěryhodné webové stránky mohou využívat automatické vyplňování při načítání stránky.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Kompromitované nebo nedůvěryhodné webové stránky mohou zneužívat automatické vyplňování při načítání stránky." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Další informace o rizicích" + }, "learnMoreAboutAutofill": { "message": "Více informací o automatickém vyplňování" }, @@ -1238,9 +1429,15 @@ "commandOpenSidebar": { "message": "Otevřít trezor v postranním panelu" }, - "commandAutofillDesc": { + "commandAutofillLoginDesc": { "message": "Automaticky vyplní poslední použité přihlašovací údaje pro tuto stránku." }, + "commandAutofillCardDesc": { + "message": "Automaticky vyplní poslední použitou kartu pro tuto stránku." + }, + "commandAutofillIdentityDesc": { + "message": "Automaticky vyplní poslední použitou identitu pro tuto stránku." + }, "commandGeneratePasswordDesc": { "message": "Vygeneruje a zkopíruje nové náhodné heslo do schránky." }, @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Zaškrtávací políčko" + }, "cfTypeLinked": { "message": "Propojené", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "Zobrazit $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Historie hesel" }, @@ -1533,6 +1742,10 @@ "message": "Základní doména", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Základní doména (doporučeno)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Název domény", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Zjišťování shody", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Výchozí zjišťování shody", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Přepnout volby" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Nejsou k dispozici žádná hesla." }, + "clearHistory": { + "message": "Vymazat historii" + }, + "noPasswordsToShow": { + "message": "Žádná hesla k zobrazení" + }, + "noRecentlyGeneratedPassword": { + "message": "Nedávno jste nevygenerovali heslo" + }, "remove": { "message": "Odebrat" }, @@ -1677,6 +1899,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Jedna nebo více zásad organizace ovlivňují nastavení generátoru." }, + "passwordGenerator": { + "message": "Generátor hesla" + }, + "usernameGenerator": { + "message": "Generátor uživatelského jména" + }, + "useThisPassword": { + "message": "Použít toto heslo" + }, + "useThisUsername": { + "message": "Použít toto uživatelské jméno" + }, + "securePasswordGenerated": { + "message": "Bezpečné heslo bylo vygenerováno! Nezapomeňte také aktualizovat heslo na webu." + }, + "useGeneratorHelpTextPartOne": { + "message": "Použijte generátor", + "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": "pro vytvoření silného jedinečného hesla", + "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" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Vaše nové hlavní heslo nesplňuje požadavky zásad organizace." }, - "receiveMarketingEmails": { - "message": "Získejte e-maily od Bitwardenu pro oznámení, poradenství a výzkumné příležitosti." + "receiveMarketingEmailsV2": { + "message": "Dostávejte do své e-mailové schránky rady, oznámení a příležitosti k výzkumu od společnosti Bitwarden." }, "unsubscribe": { "message": "Odhlásit odběr" @@ -1874,6 +2119,12 @@ "nativeMessagingWrongUserTitle": { "message": "Neshoda účtů" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Biometrické odemknutí se nezdařilo. Zkuste znovu nastavit biometrické prvky." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Neshoda biometrického klíče" + }, "biometricsNotEnabledTitle": { "message": "Biometrie není nastavena" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Odemkněte tohoto uživatele v programu pro počítač a zkuste to znovu." }, + "biometricsNotAvailableTitle": { + "message": "Biometrické odemknutí není k dispozici" + }, + "biometricsNotAvailableDesc": { + "message": "Biometrické odemknutí je momentálně nedostupné. Opakujte akci později." + }, "biometricsFailedTitle": { "message": "Biometrika selhala" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "Zásady organizace zablokovaly importování položek do Vašeho osobního trezoru." }, + "domainsTitle": { + "message": "Domény", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Vyloučené domény" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden nebude žádat o uložení přihlašovacích údajů pro tyto domény pro všechny přihlášené účty. Aby se změny projevily, musíte stránku obnovit." }, + "websiteItemLabel": { + "message": "Webová stránka $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ není platná doména", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Vyloučené změny domény byly uloženy" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Chráněno heslem" }, + "copyLink": { + "message": "Kopírovat odkaz" + }, "copySendLink": { "message": "Zkopírovat odkaz Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send vytvořen", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send byl úspěšně vytvořen!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "Send bude k dispozici na příštích $DAYS$ dnů každému, kdo má odkaz.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Odkaz Send byl zkopírován", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send upraven", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Je vyžadováno ověření e-mailu" }, + "emailVerifiedV2": { + "message": "E-mail byl ověřen" + }, "emailVerificationRequiredDesc": { "message": "Abyste mohli tuto funkci používat, musíte ověřit svůj e-mail. Svůj e-mail můžete ověřit ve webovém trezoru." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatická registrace" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Nastavení automatického vyplňování" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Zkrátka automatického vyplňování" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Změnit zkratku" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Spravovat zástupce" + }, "autofillShortcut": { - "message": "Klávesová kombinace pro automatické vyplňování" + "message": "Klávesová zkratka pro automatické vyplňování" }, - "autofillShortcutNotSet": { - "message": "Klávesová kombinace pro automatické vyplňování není nastavena. Změňte ji v nastavení prohlížeče." + "autofillLoginShortcutNotSet": { + "message": "Klávesová zkratka pro automatické vyplnění přihlášení není nastavena. Změňte ji v nastavení prohlížeče." }, - "autofillShortcutText": { - "message": "Klávesová kombinace pro automatické vyplňování je: $COMMAND$. Změňte ji v nastavení prohlížeče.", + "autofillLoginShortcutText": { + "message": "Klávesová zkratka pro automatické vyplnění přihlášení je $COMMAND$. Spravujte všechny zkratky v nastavení prohlížeče.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Výchozí klávesová kombinace pro automatické vyplňování: $COMMAND$.", + "message": "Výchozí klávesová zkratka pro automatické vyplňování: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Zařízení zařazeno mezi důvěryhodné" }, + "sendsNoItemsTitle": { + "message": "Žádná aktivní Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Použijte Send pro bezpečné sdílení šifrovaných informací s kýmkoliv.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Je vyžadován vstup." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 pole vyžaduje Vaši pozornost." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ polí vyžaduje Vaši pozornost.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Vybrat --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Položky se žádostí o změnu hlavního hesla nemohou být automaticky vyplněny při načítání stránky. Automatické vyplnění při načítání stránky je vypnuto.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "Automatické vyplnění při načítání stránky bylo nastaveno na výchozí nastavení.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Pro úpravu tohoto pole vypněte požadavek na hlavní heslo", @@ -2911,10 +3240,18 @@ "message": "Odemkněte Váš účet pro zobrazení odpovídajících přihlašovacích údajů", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Odemkněte Váš účet pro zobrazení návrhů automatického vyplňování", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Odemknout účet", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Odemknout účet, otevře se v novém okně", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Vyplnit přihlašovací údaje pro", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Přidat novou položku trezoru", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "Nové přihlášení", + "description": "Button text to display within inline menu when there are no matching items on a login field" + }, + "addNewLoginItemAria": { + "message": "Přidat do trezoru novou položku pro přihlášení, otevře se v novém okně", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "Nová karta", + "description": "Button text to display within inline menu when there are no matching items on a credit card field" + }, + "addNewCardItemAria": { + "message": "Přidat do trezoru novou položku karty, otevře se v novém okně", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "Nová identita", + "description": "Button text to display within inline menu when there are no matching items on an identity field" + }, + "addNewIdentityItemAria": { + "message": "Přidat do trezoru novou identitu, otevře se v novém okně", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Nabídka automatického vyplňování Bitwardenu. Pro výběr stiskněte šipku dolů.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Chyba při připojování ke službě Duo. Použijte jinou dvoufázovou metodu přihlášení nebo kontaktujte Duo o pomoc." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Spusťte DUO a pro dokončení přihlášení postupujte podle kroků." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Neplatné heslo souboru, použijte heslo zadané při vytvoření souboru exportu." }, - "importDestination": { - "message": "Cíl importu" + "destination": { + "message": "Cíl" }, "learnAboutImportOptions": { "message": "Více o volbách importu" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Ověření vyžadované iniciátorem webu. Tato funkce ještě není implementována pro účty bez hlavního hesla." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Přihlásit se pomocí přístupového klíče?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "Pro tuto stránku nemáte žádné přihlašovací údaje." }, + "noMatchingLoginsForSite": { + "message": "Žádné odpovídající přihlašovací údaje pro tento web" + }, "confirm": { "message": "Potvrdit" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Uložit přístupový klíč jako nové přihlášení" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Vyberte přihlášení pro uložení tohoto přístupového klíče" }, + "chooseCipherForPasskeyAuth": { + "message": "Vyberte přístupový klíč, kterým se chcete přihlásit" + }, "passkeyItem": { "message": "Položka přístupového klíče" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,6 +3659,30 @@ "message": "Společné formáty", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Pokračovat do nastavení prohlížeče?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Pokračovat do Centra nápovědy?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" + }, + "confirmContinueToHelpCenterPasswordManagementContent": { + "message": "Změňte nastavení automatického vyplňování a správy hesel.", + "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": "Můžete zobrazit a nastavit zkratky rozšíření v nastavení prohlížeče.", + "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": "Změňte nastavení automatického vyplňování a správy hesel.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" + }, + "confirmContinueToBrowserKeyboardShortcutSettingsContent": { + "message": "Můžete zobrazit a nastavit zkratky rozšíření v nastavení prohlížeče.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Nastavit Bitwarden jako výchozí správce hesel?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" @@ -3317,10 +3711,18 @@ "message": "Pověření byla úspěšně uložena!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Heslo uloženo!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Pověření byla úspěšně aktualizována!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Heslo aktualizováno!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Chyba při ukládání přihlašovacích údajů. Podrobnosti naleznete v konzoli.", "description": "Notification message for when saving credentials has failed." @@ -3334,20 +3736,6 @@ "passkeyRemoved": { "message": "Přístupový klíč byl odebrán" }, - "unassignedItemsBannerNotice": { - "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve vašem zobrazení všech trezorů a jsou nyní přístupné pouze v konzoli správce." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Upozornění: 16. květba 2024 již nebudou nepřiřazené položky organizace viditelné ve vašem zobrazení všech trezorů a budou přístupné pouze v konzoli správce." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Přiřadit tyto položky ke kolekci z", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "aby byly viditelné.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Návrhy automatického vyplňování" }, @@ -3415,7 +3803,7 @@ }, "autofillTitle": { "message": "Automatické vyplnění - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "Žádné hodnoty ke zkopírování" }, - "assignCollections": { - "message": "Přiřadit kolekce" + "assignToCollections": { + "message": "Přiřadit ke kolekcím" }, "copyEmail": { "message": "Kopírovat e-mail" @@ -3493,13 +3881,13 @@ "message": "Položky bez složky" }, "itemDetails": { - "message": "Item details" + "message": "Detaily položky" }, "itemName": { - "message": "Item name" + "message": "Název položky" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Nemůžete odebrat kolekce s oprávněními jen pro zobrazení: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3899,33 @@ "message": "Organizace je deaktivována" }, "owner": { - "message": "Owner" + "message": "Vlastník" }, "selfOwnershipLabel": { - "message": "You", + "message": "Vy", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "K položkám v deaktivované organizaci nemáte přístup. Požádejte o pomoc vlastníka organizace." }, + "additionalInformation": { + "message": "Další informace" + }, + "itemHistory": { + "message": "Historie položky" + }, + "lastEdited": { + "message": "Naposledy upraveno" + }, + "ownerYou": { + "message": "Vlastník: Vy" + }, + "linked": { + "message": "Propojeno" + }, + "copySuccessful": { + "message": "Kopírování bylo úspěšné" + }, "upload": { "message": "Nahrát" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filtry" + }, + "personalDetails": { + "message": "Osobní údaje" + }, + "identification": { + "message": "Identifikace" + }, + "contactInfo": { + "message": "Kontaktní informace" + }, + "downloadAttachment": { + "message": "Stahování - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "číslo karty končí", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Přihlašovací údaje" + }, + "authenticatorKey": { + "message": "Ověřovací klíč" + }, + "autofillOptions": { + "message": "Volby automatického vyplňování" + }, + "websiteUri": { + "message": "Webová stránka (URI)" + }, + "websiteUriCount": { + "message": "Webová stránka (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": "Webová stránka přidána" + }, + "addWebsite": { + "message": "Přidat webovou stránku" + }, + "deleteWebsite": { + "message": "Vymazat webovou stránku" + }, + "defaultLabel": { + "message": "Výchozí ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Zobrazit detekci shody $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Skrýt detekci shody $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Automaticky vyplnit při načtení stránky?" + }, + "cardExpiredTitle": { + "message": "Prošlá karta" + }, + "cardExpiredMessage": { + "message": "Pokud jste ji obnovili, aktualizujte informace o kartě" + }, + "cardDetails": { + "message": "Podrobnosti karty" + }, + "cardBrandDetails": { + "message": "Podrobnosti o $BRAND$", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Povolit animace" + }, + "addAccount": { + "message": "Přidat účet" + }, + "loading": { + "message": "Načítání" + }, + "data": { + "message": "Data" + }, + "passkeys": { + "message": "Přístupové klíče", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Hesla", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Přihlásit se pomocí přístupového klíče", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, + "assign": { + "message": "Přiřadit" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Jen členové organizace s přístupem k těmto kolekcím budou moci vidět položku." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Jen členové organizace s přístupem k těmto kolekcím budou moci vidět položky." + }, + "bulkCollectionAssignmentWarning": { + "message": "Vybrali jste $TOTAL_COUNT$ položek. Nemůžete aktualizovat $READONLY_COUNT$ položek, protože nemáte oprávnění k úpravám.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Přidat pole" + }, + "add": { + "message": "Přidat" + }, + "fieldType": { + "message": "Typ pole" + }, + "fieldLabel": { + "message": "Popis pole" + }, + "textHelpText": { + "message": "Použijte textová pole pro data jako bezpečnostní otázky" + }, + "hiddenHelpText": { + "message": "Použijte skrytá pole pro citlivá data, jako je heslo" + }, + "checkBoxHelpText": { + "message": "Použijte zaškrtávací políčka, pokud chcete automaticky vyplnit zaškrtávací políčko formuláře (např. pro zapamatování e-mailu)" + }, + "linkedHelpText": { + "message": "Použijte propojené pole, pokud máte problémy s automatickým vyplňováním na konkrétní webové stránce." + }, + "linkedLabelHelpText": { + "message": "Zadejte ID pole z HTML, název, popisek nebo zástupný znak pole." + }, + "editField": { + "message": "Upravit pole" + }, + "editFieldLabel": { + "message": "Upravit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Smazat $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ - přidáno", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Změnit pořadí $LABEL$. Použijte šipky pro posunutí položky nahoru nebo dolů.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ - přesunuto nahoru, pozice $INDEX$ z $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Vyberte kolekce pro přiřazení" + }, + "personalItemTransferWarningSingular": { + "message": "1 položka bude trvale převedena do vybrané organizace. Tuto položku již nebudete vlastnit." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ položek bude trvale převedeno do vybrané organizace. Tyto položky již nebudete vlastnit.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 položka bude trvale převedena do $ORG$. Tuto položku již nebudete vlastnit.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ položek bude trvale převedeno do $ORG$. Tyto položky již nebudete vlastnit.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Kolekce byly úspěšně přiřazeny" + }, + "nothingSelected": { + "message": "Nevybrali jste žádné položky." + }, + "movedItemsToOrg": { + "message": "Vybrané položky přesunuty do $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Položky přesunuty do $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Položka přesunuta do $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ - přesunuto dolů, pozice $INDEX$ z $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Umístění položky" + }, + "fileSends": { + "message": "Sends se soubory" + }, + "textSends": { + "message": "Sends s texty" + }, + "bitwardenNewLook": { + "message": "Bitwarden má nový vzhled!" + }, + "bitwardenNewLookDesc": { + "message": "Je snazší a intuitivnější než kdy jindy automaticky vyplňovat a vyhledávat z karty trezor. Mrkněte se!" + }, + "accountActions": { + "message": "Činnosti účtu" + }, + "showNumberOfAutofillSuggestions": { + "message": "Zobrazit počet návrhů automatického vyplňování přihlášení na ikoně rozšíření" + }, + "systemDefault": { + "message": "Systémový výchozí" + }, + "enterprisePolicyRequirementsApplied": { + "message": "Na toto nastavení byly uplatněny požadavky podnikových zásad" + }, + "fileSavedToDevice": { + "message": "Soubor byl uložen. Můžete jej nalézt ve stažené složce v zařízení." + }, + "showCharacterCount": { + "message": "Zobrazit počet znaků" + }, + "hideCharacterCount": { + "message": "Skrýt počet znaků" + }, + "itemsInTrash": { + "message": "Položky v koši" + }, + "noItemsInTrash": { + "message": "Žádné položky v koši" + }, + "noItemsInTrashDesc": { + "message": "Položky, které smažete, se zde zobrazí a budou trvale smazány po 30 dnech." + }, + "trashWarning": { + "message": "Položky, které byly v koši déle než 30 dní, budou automaticky smazány." + }, + "restore": { + "message": "Obnovit" + }, + "deleteForever": { + "message": "Smazat navždy" + }, + "noEditPermissions": { + "message": "Nemáte oprávnění upravit tuto položku" } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 3890f1f8a9c..e43e81e1e50 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -13,18 +13,18 @@ "loginOrCreateNewAccount": { "message": "Mewngofnodwch neu crëwch gyfrif newydd i gael mynediad i'ch cell ddiogel." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "Creu cyfrif" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Gosod cyfrinair cryf" }, "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "Mewngofnodi" - }, "enterpriseSingleSignOn": { "message": "Enterprise single sign-on" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Master password hint (optional)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "Tab" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Copy security code" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "Llenwi'n awtomatig" }, @@ -150,7 +171,7 @@ "message": "Mewngofnodi i'ch cell" }, "autoFillInfo": { - "message": "There are no logins available to auto-fill for the current browser tab." + "message": "There are no logins available to autofill for the current browser tab." }, "addLogin": { "message": "Ychwanegu manylion mewngofnodi" @@ -239,7 +260,7 @@ "message": "Bitwarden for Business" }, "bitwardenAuthenticator": { - "message": "Bitwarden Authenticator" + "message": "Dilyswr 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" @@ -280,6 +301,24 @@ "editFolder": { "message": "Golygu ffolder" }, + "newFolder": { + "message": "Ffolder newydd" + }, + "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" }, @@ -345,16 +384,56 @@ "message": "Hyd lleiaf cyfrineiriau" }, "uppercase": { - "message": "Priflythrennau (A-Z)" + "message": "Priflythrennau (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Llythrennau bach (a-z)" + "message": "Llythrennau bach (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Rhifau (0-9)" + "message": "Rhifau (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Nodau arbennig (!@#$%^&*)" + "message": "Nodau arbennig (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Nifer o eiriau" @@ -376,7 +455,12 @@ "message": "Isafswm nodau arbennig" }, "avoidAmbChar": { - "message": "Osgoi nodau amwys" + "message": "Osgoi nodau amwys", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Chwilio'r gell" @@ -454,7 +538,7 @@ "message": "Gosodiadau eraill" }, "unlockMethods": { - "message": "Unlock options" + "message": "Dewisiadau datgloi" }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." @@ -466,7 +550,7 @@ "message": "Session timeout" }, "otherOptions": { - "message": "Other options" + "message": "Dewisiadau eraill" }, "rateExtension": { "message": "Rhoi eich barn ar yr estyniad" @@ -556,6 +640,18 @@ "security": { "message": "Diogelwch" }, + "confirmMasterPassword": { + "message": "Cadarnhau'r prif gyfrinair" + }, + "masterPassword": { + "message": "Prif gyfrinair" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Bu gwall" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Mae eich cyfrif newydd wedi cael ei greu! Gallwch bellach fewngofnodi." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Mae angen cod dilysu." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Cod dilysu annilys" }, @@ -613,7 +718,7 @@ } }, "autofillError": { - "message": "Unable to auto-fill the selected item on this page. Copy and paste the information instead." + "message": "Unable to autofill the selected item on this page. Copy and paste the information instead." }, "totpCaptureError": { "message": "Unable to scan QR code from the current webpage" @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Mae eich sesiwn wedi dod i ben." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Ydych chi'n siŵr eich bod am allgofnodi?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "URI newydd" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "Eitem wedi'i hychwanegu" }, @@ -737,23 +873,32 @@ "enableAddLoginNotification": { "message": "Ask to add login" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "Ask to add an item if one isn't found in your vault." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Show cards on Tab page" }, "showCardsCurrentTabDesc": { - "message": "List card items on the Tab page for easy auto-fill." + "message": "List card items on the Tab page for easy autofill." + }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" }, "showIdentitiesCurrentTabDesc": { - "message": "List identity items on the Tab page for easy auto-fill." + "message": "List identity items on the Tab page for easy autofill." }, "clearClipboard": { "message": "Clirio'r clipfwrdd", @@ -791,13 +936,13 @@ "message": "Diweddaru" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Datgloi" }, "additionalOptions": { - "message": "Additional options" + "message": "Dewisiadau ychwanegol" }, "enableContextMenuItem": { "message": "Show context menu options" @@ -810,10 +955,10 @@ }, "defaultUriMatchDetection": { "message": "Default URI match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { - "message": "Choose the default way that URI match detection is handled for logins when performing actions such as auto-fill." + "message": "Choose the default way that URI match detection is handled for logins when performing actions such as autofill." }, "theme": { "message": "Thema" @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "Storfa 1GB wedi'i hamgryptio ar gyfer atodiadau ffeiliau." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Dewisiadau mewngofnodi dau gam perchenogol megis YubiKey a Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Mae gennych aelodaeth uwch!" }, "premiumCurrentMemberThanks": { "message": "Diolch am gefnogi Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "Hyn oll am $PRICE$ y flwyddyn!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Refresh complete" }, @@ -1028,7 +1191,7 @@ "message": "Copy TOTP automatically" }, "disableAutoTotpCopyDesc": { - "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login." + "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you autofill the login." }, "enableAutoBiometricsPrompt": { "message": "Ask for biometrics on launch" @@ -1178,10 +1341,19 @@ "message": "Environment URLs saved" }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { @@ -1199,17 +1371,36 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "enableAutoFillOnPageLoadDesc": { "message": "Llenwi'n awtomatig wrth i dudalen lwytho os canfyddir ffurflen mewngofnodi." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Compromised or untrusted websites can exploit autofill on page load." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" }, "learnMoreAboutAutofill": { "message": "Dysgu mwy am lenwi'n awtomatig" @@ -1218,19 +1409,19 @@ "message": "Default autofill setting for login items" }, "defaultAutoFillOnPageLoadDesc": { - "message": "You can turn off auto-fill on page load for individual login items from the item's Edit view." + "message": "You can turn off autofill on page load for individual login items from the item's Edit view." }, "itemAutoFillOnPageLoad": { - "message": "Auto-fill on page load (if set up in Options)" + "message": "Autofill on page load (if set up in Options)" }, "autoFillOnPageLoadUseDefault": { "message": "Defnyddio'r gosodiad rhagosodedig" }, "autoFillOnPageLoadYes": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "autoFillOnPageLoadNo": { - "message": "Do not auto-fill on page load" + "message": "Do not autofill on page load" }, "commandOpenPopup": { "message": "Open vault popup" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Open vault in sidebar" }, - "commandAutofillDesc": { - "message": "Auto-fill the last used login for the current website" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Generate and copy a new random password to the clipboard" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Gwerth Boole" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Linked", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Hanes cyfrineiriau" }, @@ -1533,6 +1742,10 @@ "message": "Base domain", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domain name", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Cymharu URIs", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Default match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Toggle options" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Does dim cyfrineiriau i'w rhestru." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Tynnu" }, @@ -1677,6 +1899,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" }, @@ -1722,10 +1967,10 @@ "message": "Fill and save" }, "autoFillSuccessAndSavedUri": { - "message": "Item auto-filled and URI saved" + "message": "Item autofilled and URI saved" }, "autoFillSuccess": { - "message": "Item auto-filled " + "message": "Item autofilled " }, "insecurePageWarning": { "message": "Warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page." @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Parthau wedi'u heithrio" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "Dyw $DOMAIN$ ddim yn barth dilys", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Password protected" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Copy Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send created", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2606,7 +2906,7 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { "message": "Sut i lenwi'n awtomatig" @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Gosodiadau llenwi awtomatig" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" + }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2693,13 +3002,13 @@ "message": "and continue creating your account." }, "noEmail": { - "message": "No email?" + "message": "Dim ebost?" }, "goBack": { - "message": "Go back" + "message": "Ewch yn ôl" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "i olygu eich cyfeiriad ebost." }, "eu": { "message": "UE", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Dewis --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Neidio i'r cynnwys" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { - "message": "Unlock your account to view matching logins", + "message": "Datglowch eich cyfrif i weld manylion mewngofnodi sy'n cyfateb", + "description": "Text to display in overlay when the account is locked." + }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", "description": "Text to display in overlay when the account is locked." }, "unlockAccount": { - "message": "Unlock account", + "message": "Datgloi eich cyfrif", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Ychwanegu eitem newydd i'r gell", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -2980,7 +3341,7 @@ "message": "Verification required for this action. Set a PIN to continue." }, "setPin": { - "message": "Set PIN" + "message": "Gosod PIN" }, "verifyWithBiometrics": { "message": "Verify with biometrics" @@ -2998,10 +3359,10 @@ "message": "Use master password" }, "usePin": { - "message": "Use PIN" + "message": "Defnyddio PIN" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Defnyddio biometreg" }, "enterVerificationCodeSentToEmail": { "message": "Enter the verification code that was sent to your email." @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,11 +3412,11 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { - "message": "Learn about your import options" + "message": "Dysgu am eich dewisiadau mewnforio" }, "selectImportFolder": { "message": "Select a folder" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Cadarnhau" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Fformatau cyffredin", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Hoffech chi wneud Bitwarden yn rheolydd cyfrineiriau rhagosodedig?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3448,7 +3836,7 @@ "message": "Notifications" }, "appearance": { - "message": "Appearance" + "message": "Golwg" }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Ychwanegu cyfrif" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Ychwanegu" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index d844273f622..8771b323cb7 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Log ind eller opret en ny konto for at få adgang til din sikre boks." }, + "inviteAccepted": { + "message": "Invitation accepteret" + }, "createAccount": { "message": "Opret konto" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Afslut kontooprettelsen med at indstille en adgangskode" }, - "login": { - "message": "Log ind" - }, "enterpriseSingleSignOn": { "message": "Virksomheds Single-Sign-On" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Hovedadgangskodetip (valgfrit)" }, + "joinOrganization": { + "message": "Bliv medlem af organisation" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Færdiggør tilmeldingen til denne organisation ved at opsætte en hovedadgangskode." + }, "tab": { "message": "Fane" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Kopiér sikkerhedskode" }, + "copyName": { + "message": "Kopiér navn" + }, + "copyCompany": { + "message": "Kopiér virksomhed" + }, + "copySSN": { + "message": "Kopiér CPR-nummer" + }, + "copyPassportNumber": { + "message": "Kopiér pasnummer" + }, + "copyLicenseNumber": { + "message": "Kopier licensnummer" + }, "autoFill": { "message": "Auto-udfyld" }, @@ -280,6 +301,24 @@ "editFolder": { "message": "Redigér mappe" }, + "newFolder": { + "message": "Ny mappe" + }, + "folderName": { + "message": "Mappenavn" + }, + "folderHintText": { + "message": "Indlejr en mappe ved at tilføje den overordnede mappes navn efterfulgt af en “/”. F.eks.: Social/Fora" + }, + "noFoldersAdded": { + "message": "Ingen mapper tilføjet" + }, + "createFoldersToOrganize": { + "message": "Opret mapper for at organisere boks-emner" + }, + "deleteFolderPermanently": { + "message": "Sikker på, at denne mappe skal slettes permanent?" + }, "deleteFolder": { "message": "Slet mappe" }, @@ -345,16 +384,56 @@ "message": "Minimumslængde på adgangskode" }, "uppercase": { - "message": "Store bogstaver (A-Z)" + "message": "Store bogstaver (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Små bogstaver (a-z)" + "message": "Små bogstaver (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Tal (0-9)" + "message": "Tal (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Specialtegn (!@#$%^&*)" + "message": "Specialtegn (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Inkludér", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Inkludér majuskler", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Inkludér minuskler", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Inkludér tal", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Inkludér specialtegn", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Antal ord" @@ -376,7 +455,12 @@ "message": "Mindste antal specialtegn" }, "avoidAmbChar": { - "message": "Undgå tvetydige tegn" + "message": "Undgå tvetydige tegn", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Undgå tvetydige tegn", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Søg i boks" @@ -556,6 +640,18 @@ "security": { "message": "Sikkerhed" }, + "confirmMasterPassword": { + "message": "Bekræft hovedadgangskode" + }, + "masterPassword": { + "message": "Hovedadgangskode" + }, + "masterPassImportant": { + "message": "Hovedadgangskoden kan ikke gendannes, hvis den glemmes!" + }, + "masterPassHintLabel": { + "message": "Hovedadgangskodetip" + }, "errorOccurred": { "message": "Der er opstået en fejl" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Din nye konto er oprettet! Du kan nu logge ind." }, + "newAccountCreated2": { + "message": "Din nye konto er oprettet!" + }, + "youHaveBeenLoggedIn": { + "message": "Du er blevet logget ind!" + }, "youSuccessfullyLoggedIn": { "message": "Du er nu logget ind" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Bekræftelseskode er påkrævet." }, + "webauthnCancelOrTimeout": { + "message": "Godkendelsen blev afbrudt eller tog for lang tid. Forsøg igen." + }, "invalidVerificationCode": { "message": "Ugyldig bekræftelseskode" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Skan godkendelses QR-kode fra den aktuelle webside" }, + "totpHelperTitle": { + "message": "Gør 2-trinsbekræftelse problemfri" + }, + "totpHelper": { + "message": "Bitwarden kan gemme og udfylde 2-trinsbekræftelseskoder. Kopiér og indsæt nøglen i dette felt." + }, + "totpHelperWithCapture": { + "message": "Bitwarden kan gemme og udfylde 2-trinsbekræftelseskoder. Vælg kameraikonet for at tage et skærmfoto af dette websteds godkendelses-QR-kode, eller kopiér og indsæt nøglen i dette felt." + }, + "learnMoreAboutAuthenticators": { + "message": "Læs mere om autentifikatorer" + }, "copyTOTP": { "message": "Kopiér godkendelsesnøgle (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Din login-session er udløbet." }, + "logIn": { + "message": "Log ind" + }, + "restartRegistration": { + "message": "Genstart registrering" + }, + "expiredLink": { + "message": "Udløbet link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Genstart registreringen eller prøv at logge ind." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Har allerede oprettet en konto?" + }, "logOutConfirmation": { "message": "Er du sikker på, du vil logge ud?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Ny URI" }, + "addDomain": { + "message": "Tilføj domæne", + "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": { "message": "Element tilføjet" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Spørg om at tilføje login" }, + "vaultSaveOptionsTitle": { + "message": "Gem i boksindstillinger" + }, "addLoginNotificationDesc": { "message": "Spørg om at tilføje et element, hvis et ikke findes i din boks." }, "addLoginNotificationDescAlt": { "message": "Anmod om at tilføje et emne, hvis intet ikke findes i boksen. Gælder alle indloggede konti." }, + "showCardsInVaultView": { + "message": "Vis kort som Autoudfyldningsforslag ved Boks-visning" + }, "showCardsCurrentTab": { "message": "Vis kort på fanebladet" }, "showCardsCurrentTabDesc": { "message": "Vis kortelementer på fanebladet for nem auto-udfyldning." }, + "showIdentitiesInVaultView": { + "message": "Vis identiteter som Autoudfyldningsforslag ved Boks-visning" + }, "showIdentitiesCurrentTab": { "message": "Vis identiteter på fanebladet" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Standard URI matchmetode", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Vælg den standard måde, som URI matchmetode håndteres til logins, når du udfører handlinger som f.eks. Auto-udfyld." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB krypteret lager til vedhæftede filer." }, + "premiumSignUpEmergency": { + "message": "Nødadgang" + }, "premiumSignUpTwoStepOptions": { "message": "Proprietære totrins-login muligheder, såsom YubiKey og Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Du kan købe premium-medlemskab i bitwarden.com web-boksen. Vil du besøge hjemmesiden nu?" }, + "premiumPurchaseAlertV2": { + "message": "Der kan købes Premium fra kontoindstillingerne via Bitwarden web-appen." + }, "premiumCurrentMember": { "message": "Du er premium-medlem!" }, "premiumCurrentMemberThanks": { "message": "Tak fordi du støtter Bitwarden." }, + "premiumFeatures": { + "message": "Opgradér til Premium og modtag:" + }, "premiumPrice": { "message": "Alt dette for kun $PRICE$ /år!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "Alt dette for kun $PRICE$ pr. år!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Opdatering færdig" }, @@ -1179,9 +1342,18 @@ }, "showAutoFillMenuOnFormFields": { "message": "Vis autoudfyld-menu i formularfelter", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autoudfyldningsforslag" + }, + "showInlineMenuLabel": { + "message": "Vis autoudfyld-menu i formularfelter" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Vis forslag, når ikonet vælges" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Gælder for alle indloggede konti." }, "turnOffBrowserBuiltInPasswordManagerSettings": { @@ -1202,14 +1374,33 @@ "message": "Når autoudfyld-ikon vælges", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autoudfyld ved sideindlæsning" + }, "enableAutoFillOnPageLoad": { "message": "Auto-udfyld ved sideindlæsning" }, "enableAutoFillOnPageLoadDesc": { "message": "Hvis der registreres en loginformular, så auto-udfyld, når websiden indlæses." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Advarsel:$CLOSETAG$ Kompromitterede eller ikke-betroede websteder kan misbruge autofyld ved sideindlæsning.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { - "message": "Kompromitterede eller ikke-betroede websteder kan udnytte autoudfyldning ved sideindlæsning." + "message": "Kompromitterede eller ikke-betroede websteder kan misbruge autoudfyldning ved sideindlæsning." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Læs mere om risici" }, "learnMoreAboutAutofill": { "message": "Læs mere om autoudfyldning" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Åbn boks i sidebjælken" }, - "commandAutofillDesc": { - "message": "Auto-udfyld det sidste anvendte login for den aktuelle hjemmeside" + "commandAutofillLoginDesc": { + "message": "Autoudfyld det senest anvendte login for det aktuelle websted" + }, + "commandAutofillCardDesc": { + "message": "Autoudfyld det senest anvendte kort for det aktuelle websted" + }, + "commandAutofillIdentityDesc": { + "message": "Autoudfyld den senest anvendte identitet for det aktuelle websted" }, "commandGeneratePasswordDesc": { "message": "Generér en ny tilfældig adgangskode og kopiér den til udklipsholderen" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolsk" }, + "cfTypeCheckbox": { + "message": "Afkrydsningsfelt" + }, "cfTypeLinked": { "message": "Forbundet", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "Vis $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Adgangskodehistorik" }, @@ -1533,6 +1742,10 @@ "message": "Grund-domæne", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Basisdomæne (anbefalet)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domænenavn", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Matchmetode", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Standard matchmetode", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Skift indstillinger" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Der er ingen kodeord at vise." }, + "clearHistory": { + "message": "Ryd historik" + }, + "noPasswordsToShow": { + "message": "Ingen adgangskoder at vise" + }, + "noRecentlyGeneratedPassword": { + "message": "Der er ikke genereret nogen adgangskode for nylig" + }, "remove": { "message": "Fjern" }, @@ -1677,6 +1899,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" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Din nye hovedadgangskode opfylder ikke politikkravene." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Få råd, bekendtgørelser og forskningsmuligheder fra Bitwarden i indbakken." }, "unsubscribe": { "message": "Afmeld" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Oplås denne bruger op i computerprogrammet og forsøg igen." }, + "biometricsNotAvailableTitle": { + "message": "Biometrisk oplåsning utilgængelig" + }, + "biometricsNotAvailableDesc": { + "message": "Biometrisk oplåsning er p.t. ikke tilgængelig. Prøv igen senere." + }, "biometricsFailedTitle": { "message": "Biometri mislykkedes" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "En organisationspolitik hindrer import af emner til den individuelle boks." }, + "domainsTitle": { + "message": "Domæner", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Ekskluderede domæner" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden vil ikke anmode om at gemme login-detaljer for disse domæner for alle indloggede konti. Siden skal opfriskes for at effektuere ændringerne." }, + "websiteItemLabel": { + "message": "Websted $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ er ikke et gyldigt domæne", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Ekskluderet domæne-ændringer gemt" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Kodeordsbeskyttet" }, + "copyLink": { + "message": "Kopier link" + }, "copySendLink": { "message": "Kopiér Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send oprettet", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send er hermed oprettet!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "Denne Send vil være tilgængelig for alle med linket i de næste $DAYS$ dage.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send-link kopieret", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send gemt", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "E-mailbekræftelse kræves" }, + "emailVerifiedV2": { + "message": "E-mail bekræftet" + }, "emailVerificationRequiredDesc": { "message": "Du skal bekræfte din e-mail for at bruge denne funktion. Du kan bekræfte din e-mail i web-boksen." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatisk tilmelding" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Autoudfyldelsesindstillinger" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autoudfyld-genvej" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Ændre genvej" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Håndtér genveje" + }, "autofillShortcut": { "message": "Autoudfyld-tastaturgenvej" }, - "autofillShortcutNotSet": { - "message": "Autoudfyldningsgenvejen er ikke opsat. Ændr dette i browserens indstillinger." + "autofillLoginShortcutNotSet": { + "message": "Autoudfyldningsgenvejen er ikke opsat. Ændr dette i webbrowserens indstillinger." }, - "autofillShortcutText": { - "message": "Autoudfyldningsgenvejen er: $COMMAND$. Ændr dette i browserens indstillinger.", + "autofillLoginShortcutText": { + "message": "Autoudfyldningsgenvejen er $COMMAND$. Ændr dette i webbrowserens indstillinger.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Enhed betroet" }, + "sendsNoItemsTitle": { + "message": "Ingen aktive Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Brug Send til at dele krypterede oplysninger sikkert med nogen.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input obligatorisk." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 felt kræver opmærksomhed." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ felter kræver opmærksomhed.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Vælg --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Emner, hvor der anmodes om hovedadgangskode igen, kan ikke autoudfyldes ved sideindlæsning. Autoudfyldning ved sideindlæsning er slået fra.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "Autoudfyldning ved sideindlæsning sat til standardindstillingen.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Slå anmodning om hovedadgangskode igen fra for at redigere dette felt", @@ -2911,10 +3240,18 @@ "message": "Oplås kontoen for at se matchende logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Oplås kontoen for at få vist autoudfyldningsforslag", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Oplås konto", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Oplås kontoen, åbnes i et nyt vindue", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Angiv legitimationsoplysninger for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Tilføj nyt Boks-emne", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "Nyt login", + "description": "Button text to display within inline menu when there are no matching items on a login field" + }, + "addNewLoginItemAria": { + "message": "Tilføj nyt boks-login emne, åbnes i et nyt vindue", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "Nyt kort", + "description": "Button text to display within inline menu when there are no matching items on a credit card field" + }, + "addNewCardItemAria": { + "message": "Tilføj nyt boks-kort emne, åbnes i et nyt vindue", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "Ny identitet", + "description": "Button text to display within inline menu when there are no matching items on an identity field" + }, + "addNewIdentityItemAria": { + "message": "Tilføj nyt boks-identitetsemne, åbnes i et nyt vindue", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Bitwarden autoudfyld-menu tilgængelig. Tryk på pil ned-tasten for at vælge.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Fejl under forbindelsesoprettelsen til Duo-tjenesten. Brug en anden totrins-indlogningsmetode eller kontakt Duo for hjælp." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Start Duo og følg trinnene for at fuldføre indlogningen." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Ugyldig filadgangskode. Brug samme adgangskode som under oprettelsen af eksportfilen." }, - "importDestination": { - "message": "Importdestination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Læs om importmuligheder" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Bekræftelse krævet af startwebstedet. Denne funktion er endnu ikke implementeret for konti uden hovedadgangskode." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log ind med adgangsnøgle?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "Der er intet matchende login til dette websted." }, + "noMatchingLoginsForSite": { + "message": "Ingen matchende logins for dette websted" + }, "confirm": { "message": "Bekræft" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Gem adgangsnøgle som nyt login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Vælg et login at gemme denne adgangsnøgle til" }, + "chooseCipherForPasskeyAuth": { + "message": "Vælg en adgangsnøgle at logge ind med" + }, "passkeyItem": { "message": "Adgangsnøgleemne" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,6 +3659,30 @@ "message": "Almindelige formater", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Fortsæt til Browserindstillinger?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Fortsæt til Hjælpecenter?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" + }, + "confirmContinueToHelpCenterPasswordManagementContent": { + "message": "Skift browserens indstillinger for autofyldning og adgangskodehåndtering.", + "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": "Man kan se og indstille udvidelsesgenveje via Browserindstillinger.", + "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": "Skift browserens indstillinger for autofyldning og adgangskodehåndtering.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" + }, + "confirmContinueToBrowserKeyboardShortcutSettingsContent": { + "message": "Man kan se og indstille udvidelsesgenveje via Browserindstillinger.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Lad Bitwarden håndtere adgangskoder som standard?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" @@ -3317,10 +3711,18 @@ "message": "Legitimationsoplysninger er gemt!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Adgangskode gemt!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Legitimationsoplysninger er opdateret!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Adgangskode opdateret!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Fejl under import. Tjek konsollen for detaljer.", "description": "Notification message for when saving credentials has failed." @@ -3334,20 +3736,6 @@ "passkeyRemoved": { "message": "Adgangsnøgle fjernet" }, - "unassignedItemsBannerNotice": { - "message": "Bemærk: Utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Bemærk: Pr. 16. maj 2024 er utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Tildel disse emner til en samling via", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "for at gøre dem synlige.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Autoudfyldningsforslag" }, @@ -3415,7 +3803,7 @@ }, "autofillTitle": { "message": "Autoudfyld - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "Ingen værdier at kopiere" }, - "assignCollections": { - "message": "Tildel samlinger" + "assignToCollections": { + "message": "Tildel til samlinger" }, "copyEmail": { "message": "Kopiér e-mail" @@ -3493,13 +3881,13 @@ "message": "Emner uden mappe" }, "itemDetails": { - "message": "Item details" + "message": "Emnedetaljer" }, "itemName": { - "message": "Item name" + "message": "Emnenavn" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Samlinger med kun tilladelsen Vis kan ikke fjernes: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3899,33 @@ "message": "Organisation er deaktiveret" }, "owner": { - "message": "Owner" + "message": "Ejer" }, "selfOwnershipLabel": { - "message": "You", + "message": "Dig", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Emner i deaktiverede organisationer kan ikke tilgås. Kontakt organisationsejeren for assistance." }, + "additionalInformation": { + "message": "Yderligere oplysninger" + }, + "itemHistory": { + "message": "Emnehistorik" + }, + "lastEdited": { + "message": "Senest redigeret" + }, + "ownerYou": { + "message": "Ejer: Dig" + }, + "linked": { + "message": "Linket" + }, + "copySuccessful": { + "message": "Kopieret" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filtre" + }, + "personalDetails": { + "message": "Personlige oplysninger" + }, + "identification": { + "message": "Identifikation" + }, + "contactInfo": { + "message": "Kontaktoplysninger" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "kortnummer slutter med", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login-legitimationsoplysninger" + }, + "authenticatorKey": { + "message": "Godkendelsesnøgle" + }, + "autofillOptions": { + "message": "Autofill options" + }, + "websiteUri": { + "message": "Website (URI)" + }, + "websiteUriCount": { + "message": "Websted (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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autoudfyld ved sideindlæsning?" + }, + "cardExpiredTitle": { + "message": "Udløbet kort" + }, + "cardExpiredMessage": { + "message": "Er det blevet fornyet, opdatér venligst kortoplysningerne" + }, + "cardDetails": { + "message": "Kortoplysninger" + }, + "cardBrandDetails": { + "message": "$BRAND$ oplysninger", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Aktivér animationer" + }, + "addAccount": { + "message": "Tilføj konto" + }, + "loading": { + "message": "Indlæser" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Kun organisationsmedlemmer med adgang til disse samlinger vil kunne se emnet." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Kun organisationsmedlemmer med adgang til disse samlinger vil kunne se emnerne." + }, + "bulkCollectionAssignmentWarning": { + "message": "Der er valgt $TOTAL_COUNT$ emner. $READONLY_COUNT$ af emnerne kan ikke opdateres grundet manglende redigeringsrettigheder.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Tilføj felt" + }, + "add": { + "message": "Tilføj" + }, + "fieldType": { + "message": "Felttype" + }, + "fieldLabel": { + "message": "Feltetiket" + }, + "textHelpText": { + "message": "Brug tekstfelter til data, såsom sikkerhedsspørgsmål" + }, + "hiddenHelpText": { + "message": "Brug skjulte felter til sensitive data, såsom en adgangskode" + }, + "checkBoxHelpText": { + "message": "Brug afkrydsningsfelter, hvis et formularafkrydsningsfelt skal autoudfyldes, såsom en husket e-mail" + }, + "linkedHelpText": { + "message": "Brug et linket felt ved autoudfyldningsproblemer for et bestemt websted." + }, + "linkedLabelHelpText": { + "message": "Angiv feltets HTML-ID, navn, aria-label eller variabel." + }, + "editField": { + "message": "Redigér felt" + }, + "editFieldLabel": { + "message": "Redigér $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Slet $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ tilføjet", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Omarrangér $LABEL$. Brug piletasterne til at flytte elementet op eller ned.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ flyttet op, position $INDEX$ af $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Vælg samlinger at tildele" + }, + "personalItemTransferWarningSingular": { + "message": "1 emne overføres permanent til den valgte organisation. Man vil ikke længere være ejeren af dette emne." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ emner overføres permanent til den valgte organisation. Man vil ikke længere være ejeren af disse emner.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 emne overføres permanent til $ORG$. Man vil ikke længere være ejeren af dette emne.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ emner overføres permanent til $ORG$. Man vil ikke længere være ejeren af disse emner.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Samlinger hermed tildelt" + }, + "nothingSelected": { + "message": "Ingenting er valgt." + }, + "movedItemsToOrg": { + "message": "Valgte emner flyttet til $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Emner flyttet til $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Emne flyttet til $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ flyttet ned, position $INDEX$ af $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Emneplacering" + }, + "fileSends": { + "message": "Fil-Sends" + }, + "textSends": { + "message": "Tekst-Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden har fået et nyt look!" + }, + "bitwardenNewLookDesc": { + "message": "Det er lettere og mere intuitivt end nogensinde at autoudfylde og søge via fanen Boks. Tag et kig omkring!" + }, + "accountActions": { + "message": "Kontohandlinger" + }, + "showNumberOfAutofillSuggestions": { + "message": "Vis antal login-autoudfyldningsforslag på udvidelsesikon" + }, + "systemDefault": { + "message": "Systemstandard" + }, + "enterprisePolicyRequirementsApplied": { + "message": "Virksomhedspolitikkrav er anvendt på denne indstilling" + }, + "fileSavedToDevice": { + "message": "Fil gemt på enheden. Håndtér fra enhedens downloads." + }, + "showCharacterCount": { + "message": "Vis tegnantal" + }, + "hideCharacterCount": { + "message": "Skjul tegnantal" + }, + "itemsInTrash": { + "message": "Emner i papirkurv" + }, + "noItemsInTrash": { + "message": "Ingen emner i papirkurv" + }, + "noItemsInTrashDesc": { + "message": "Emner, som slettes, vil fremgå her og slettes permanent efter 30 dage" + }, + "trashWarning": { + "message": "Emner, som har været i papirkurven i over 30 dage, slettes automatisk" + }, + "restore": { + "message": "Gendan" + }, + "deleteForever": { + "message": "Slet permanent" + }, + "noEditPermissions": { + "message": "Ingen tilladelse til at redigere dette emne" } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index fcfab66a436..3af4de85e13 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Melde dich an oder erstelle ein neues Konto, um auf deinen Tresor zuzugreifen." }, + "inviteAccepted": { + "message": "Einladung angenommen" + }, "createAccount": { "message": "Konto erstellen" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Schließe die Erstellung deines Kontos ab, indem du ein Passwort festlegst" }, - "login": { - "message": "Anmelden" - }, "enterpriseSingleSignOn": { "message": "Enterprise Single-Sign-On" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Master-Passwort-Hinweis (optional)" }, + "joinOrganization": { + "message": "Organisation beitreten" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Schließe den Beitritt zu dieser Organisation ab, indem du ein Master-Passwort festlegst." + }, "tab": { "message": "Tab" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Sicherheitscode kopieren" }, + "copyName": { + "message": "Name kopieren" + }, + "copyCompany": { + "message": "Unternehmen kopieren" + }, + "copySSN": { + "message": "Sozialversicherungsnummer kopieren" + }, + "copyPassportNumber": { + "message": "Passnummer kopieren" + }, + "copyLicenseNumber": { + "message": "Lizenznummer kopieren" + }, "autoFill": { "message": "Auto-Ausfüllen" }, @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "Minimale Passwortlänge" }, "uppercase": { - "message": "Großbuchstaben (A-Z)" + "message": "Großbuchstaben (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Kleinbuchstaben (a-z)" + "message": "Kleinbuchstaben (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Zahlen (0-9)" + "message": "Zahlen (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Sonderzeichen (!@#$%^&*)" + "message": "Sonderzeichen (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Einschließen", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Großbuchstaben einschließen", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Kleinbuchstaben einschließen", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Ziffern einschließen", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Sonderzeichen einschließen", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Anzahl der Wörter" @@ -376,7 +455,12 @@ "message": "Mindestanzahl Sonderzeichen" }, "avoidAmbChar": { - "message": "Mehrdeutige Zeichen vermeiden" + "message": "Mehrdeutige Zeichen vermeiden", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Mehrdeutige Zeichen vermeiden", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Tresor durchsuchen" @@ -556,6 +640,18 @@ "security": { "message": "Sicherheit" }, + "confirmMasterPassword": { + "message": "Master-Passwort bestätigen" + }, + "masterPassword": { + "message": "Master-Passwort" + }, + "masterPassImportant": { + "message": "Dein Master-Passwort kann nicht wiederhergestellt werden, wenn du es vergisst!" + }, + "masterPassHintLabel": { + "message": "Master-Passwort-Hinweis" + }, "errorOccurred": { "message": "Ein Fehler ist aufgetaucht" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Dein neues Konto wurde erstellt! Du kannst dich jetzt anmelden." }, + "newAccountCreated2": { + "message": "Dein neues Konto wurde erstellt!" + }, + "youHaveBeenLoggedIn": { + "message": "Du wurdest angemeldet!" + }, "youSuccessfullyLoggedIn": { "message": "Du hast dich erfolgreich angemeldet." }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Verifizierungscode wird benötigt." }, + "webauthnCancelOrTimeout": { + "message": "Die Authentifizierung wurde abgebrochen oder hat zu lange gedauert. Bitte versuche es erneut." + }, "invalidVerificationCode": { "message": "Ungültiger Verifizierungscode" }, @@ -613,7 +718,7 @@ } }, "autofillError": { - "message": "Die Felder dieser Seite konnten nicht automatisch ausgefüllt werden. Bitte Nutzernamen und/oder Passwort manuell kopieren." + "message": "Die Felder dieser Seite konnten nicht automatisch ausgefüllt werden. Kopiere die Informationen und füge sie manuell ein." }, "totpCaptureError": { "message": "QR-Code kann nicht von der aktuellen Webseite gescannt werden" @@ -624,6 +729,18 @@ "totpCapture": { "message": "Den Authentifizierungs-QR-Code von der aktuellen Webseite scannen" }, + "totpHelperTitle": { + "message": "Zwei-Faktor-Authentifizierung nahtlos gestalten" + }, + "totpHelper": { + "message": "Bitwarden kann Zwei-Faktor-Authentifizierungsscodes speichern und ausfüllen. Kopiere und füge den Schlüssel in dieses Feld ein." + }, + "totpHelperWithCapture": { + "message": "Bitwarden kann Zwei-Faktor-Authentifizierungsscodes speichern und ausfüllen. Wähle das Kamerasymbol aus, um einen Screenshot des Authenticator-QR-Codes dieser Website zu machen oder kopiere und füge den Schlüssel in dieses Feld ein." + }, + "learnMoreAboutAuthenticators": { + "message": "Erfahre mehr über Authenticator-Apps" + }, "copyTOTP": { "message": "Authentifizierungsschlüssel kopieren (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Ihre Login-Sitzung ist abgelaufen." }, + "logIn": { + "message": "Anmelden" + }, + "restartRegistration": { + "message": "Registrierung neu starten" + }, + "expiredLink": { + "message": "Abgelaufener Link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Bitte starte die Registrierung erneut oder versuche dich anzumelden." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Du hast möglicherweise bereits ein Konto" + }, "logOutConfirmation": { "message": "Bist du sicher, dass du dich abmelden willst?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Neue URL" }, + "addDomain": { + "message": "Domain hinzufügen", + "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": { "message": "Eintrag hinzugefügt" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Nach dem Hinzufügen von Zugangsdaten fragen" }, + "vaultSaveOptionsTitle": { + "message": "Optionen zum Speichern im Tresor" + }, "addLoginNotificationDesc": { "message": "Nach dem Hinzufügen eines Eintrags fragen, wenn dieser nicht in deinem Tresor gefunden wurde." }, "addLoginNotificationDescAlt": { "message": "Nach dem Hinzufügen eines Eintrags fragen, wenn er nicht in deinem Tresor gefunden wurde. Gilt für alle angemeldeten Konten." }, + "showCardsInVaultView": { + "message": "Karten als Vorschläge zum Auto-Ausfüllen in der Tresor-Ansicht anzeigen" + }, "showCardsCurrentTab": { "message": "Karten auf Tab Seite anzeigen" }, "showCardsCurrentTabDesc": { "message": "Karten-Einträge auf der Tab Seite anzeigen, um das Auto-Ausfüllen zu vereinfachen." }, + "showIdentitiesInVaultView": { + "message": "Identitäten als Vorschläge zum Auto-Ausfüllen in der Tresor-Ansicht anzeigen" + }, "showIdentitiesCurrentTab": { "message": "Identitäten auf Tab Seite anzeigen" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Standard URI-Übereinstimmungserkennung", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Wähle die Standardmethode, mit der die URI-Match-Erkennung für Anmeldungen bei Aktionen wie dem automatischen Ausfüllen behandelt wird." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB verschlüsselter Speicherplatz für Dateianhänge." }, + "premiumSignUpEmergency": { + "message": "Notfallzugriff." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietäre Optionen für die Zwei-Faktor Authentifizierung wie YubiKey und Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Du kannst deine Premium-Mitgliedschaft im Bitwarden.com Web-Tresor kaufen. Möchtest du die Website jetzt besuchen?" }, + "premiumPurchaseAlertV2": { + "message": "Du kannst Premium über deine Kontoeinstellungen in der Bitwarden Web-App kaufen." + }, "premiumCurrentMember": { "message": "Du bist jetzt Premium-Mitglied!" }, "premiumCurrentMemberThanks": { "message": "Vielen Dank, dass du Bitwarden unterstützt." }, + "premiumFeatures": { + "message": "Upgrade auf Premium und erhalte:" + }, "premiumPrice": { "message": "Das alles für %price% pro Jahr!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "Alles für nur $PRICE$ pro Jahr!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Aktualisierung abgeschlossen" }, @@ -1106,17 +1269,17 @@ "message": "Authenticator App" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Gib einen Code ein, der von einer Authentifizierungs-App wie dem Bitwarden Authenticator generiert wurde.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP Security Key" + "message": "Yubico OTP-Sicherheitsschlüssel" }, "yubiKeyDesc": { "message": "Verwende einen YubiKey um auf dein Konto zuzugreifen. Funtioniert mit YubiKey 4, Nano 4, 4C und NEO Geräten." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Gib einen von Duo Security generierten Code ein.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1133,7 +1296,7 @@ "message": "E-Mail" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Gib einen an deine E-Mail-Adresse gesendeten Code ein." }, "selfHostedEnvironment": { "message": "Selbst gehostete Umgebung" @@ -1179,9 +1342,18 @@ }, "showAutoFillMenuOnFormFields": { "message": "Auto-Ausfüllen Menü in Formularfeldern anzeigen", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Vorschläge zum Auto-Ausfüllen" + }, + "showInlineMenuLabel": { + "message": "Vorschläge zum Auto-Ausfüllen in Formularfeldern anzeigen" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Vorschläge anzeigen, wenn Symbol ausgewählt ist" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Gilt für alle angemeldeten Konten." }, "turnOffBrowserBuiltInPasswordManagerSettings": { @@ -1202,15 +1374,34 @@ "message": "Wenn das Auto-Ausfüllen Symbol ausgewählt ist", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Auto-Ausfüllen beim Laden einer Seite" + }, "enableAutoFillOnPageLoad": { - "message": "Auto-Ausfüllen beim Laden einer Seite aktivieren" + "message": "Auto-Ausfüllen beim Laden der Seite" }, "enableAutoFillOnPageLoadDesc": { "message": "Wenn eine Anmeldemaske erkannt wird, die Zugangsdaten automatisch ausfüllen während die Webseite lädt." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warnung:$CLOSETAG$ Kompromittierte oder nicht vertrauenswürdige Websites können Auto-Ausfüllen beim Laden einer Seite missbrauchen.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Kompromittierte oder nicht vertrauenswürdige Websites können Auto-Ausfüllen beim Laden der Seite ausnutzen." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Erfahre mehr über Risiken" + }, "learnMoreAboutAutofill": { "message": "Erfahre mehr über Auto-Ausfüllen" }, @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Tresor in der Seitenleiste öffnen" }, - "commandAutofillDesc": { - "message": "Die zuletzt verwendeten Zugangsdaten für die aktuelle Website automatisch ausfüllen lassen" + "commandAutofillLoginDesc": { + "message": "Die zuletzt verwendeten Zugangsdaten für die aktuelle Website automatisch ausfüllen" + }, + "commandAutofillCardDesc": { + "message": "Die zuletzt verwendete Karte für die aktuelle Website automatisch ausfüllen" + }, + "commandAutofillIdentityDesc": { + "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" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Ja/Nein" }, + "cfTypeCheckbox": { + "message": "Kontrollkästchen" + }, "cfTypeLinked": { "message": "Verknüpft", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "$TYPE$ ansehen", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Passwortverlauf" }, @@ -1533,6 +1742,10 @@ "message": "Basis-Domäne", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Basis-Domain (empfohlen)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domain-Name", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Übereinstimmungserkennung", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Standard-Match-Erkennung", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Optionen umschalten" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Keine Einträge zum Anzeigen vorhanden." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Entfernen" }, @@ -1677,6 +1899,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" }, @@ -1716,7 +1961,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" @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Ihr neues Master-Passwort entspricht nicht den Anforderungen der Richtlinie." }, - "receiveMarketingEmails": { - "message": "Erhalte E-Mails von Bitwarden für Ankündigungen, Ratschläge und Forschungsmöglichkeiten." + "receiveMarketingEmailsV2": { + "message": "Erhalte Ratschläge, Ankündigungen und Marktforschungsumfragen von Bitwarden in deinem Posteingang." }, "unsubscribe": { "message": "Deabonnieren" @@ -1809,7 +2054,7 @@ "message": "jederzeit." }, "byContinuingYouAgreeToThe": { - "message": "Indem Sie fortfahren, stimmen Sie unseren" + "message": "Indem du fortfährst, stimmst du den" }, "and": { "message": "und" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Bitte entsperre diesen Nutzer in der Desktop-Anwendung und versuche es erneut." }, + "biometricsNotAvailableTitle": { + "message": "Biometrisches Entsperren nicht verfügbar" + }, + "biometricsNotAvailableDesc": { + "message": "Biometrisches Entsperren ist derzeit nicht verfügbar. Bitte versuche es später erneut." + }, "biometricsFailedTitle": { "message": "Biometrie fehlgeschlagen" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "Eine Organisationsrichtlinie hat das Importieren von Einträgen in deinen persönlichen Tresor deaktiviert." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Ausgeschlossene Domains" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden wird für alle angemeldeten Konten nicht danach fragen Zugangsdaten für diese Domains speichern. Du musst die Seite neu laden, damit die Änderungen wirksam werden." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ ist keine gültige Domain", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Änderungen der ausgeschlossenen Domain gespeichert" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Passwortgeschützt" }, + "copyLink": { + "message": "Link kopieren" + }, "copySendLink": { "message": "Send-Link kopieren", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send erstellt", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send erfolgreich erstellt!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "Das Send wird jedem mit dem Link für die nächsten $DAYS$ Tage zur Verfügung stehen.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send-Link kopiert", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send gespeichert", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "E-Mail-Verifizierung erforderlich" }, + "emailVerifiedV2": { + "message": "E-Mail-Adresse verifiziert" + }, "emailVerificationRequiredDesc": { "message": "Du musst deine E-Mail Adresse verifizieren, um diese Funktion nutzen zu können. Du kannst deine E-Mail im Web-Tresor verifizieren." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatische Registrierung" }, @@ -2606,10 +2906,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.", @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Auto-Ausfüllen Einstellungen" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Auto-Ausfüllen Tastenkombination" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Tastenkombination ändern" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Tastaturkürzel verwalten" + }, "autofillShortcut": { "message": "Auto-Ausfüllen Tastaturkürzel" }, - "autofillShortcutNotSet": { - "message": "Das Auto-Ausfüllen Tastaturkürzel ist nicht gesetzt. Du kannst eines in den Browser-Einstellungen festlegen." + "autofillLoginShortcutNotSet": { + "message": "Das Tastaturkürzel zum automatischen Ausfüllen von Zugangsdaten ist nicht festgelegt. Du kannst es in den Browser-Einstellungen ändern." }, - "autofillShortcutText": { - "message": "Das Auto-Ausfüllen Tastaturkürzel ist: $COMMAND$. Du kannst es in den Browser-Einstellungen ändern.", + "autofillLoginShortcutText": { + "message": "Das Tastaturkürzel zum automatischen Ausfüllen von Zugangsdaten ist $COMMAND$. Verwalte alle Tastaturkürzel in den Browser-Einstellungen.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Standard-Auto-Ausfüllen Tastaturkürzel: $COMMAND$.", + "message": "Standard-Auto-Ausfüllen Tastenkombination: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Gerät wird vertraut" }, + "sendsNoItemsTitle": { + "message": "Keine aktiven Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Verwende Send, um verschlüsselte Informationen sicher mit anderen zu teilen.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Eingabe ist erforderlich." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 Feld erfordert deine Aufmerksamkeit." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ Felder erfordern deine Aufmerksamkeit.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Auswählen --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Einträge, die eine erneuten Abfrage des Master-Passworts verlangen, können nicht beim Laden einer Seite automatisch ausgefüllt werden. Auto-Ausfüllen beim Laden einer Seite deaktiviert.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-Ausfüllen beim Lader einer Seite wurde auf die Standardeinstellung gesetzt.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Auto-Ausfüllen beim Laden einer Seite wurde auf die Standardeinstellung gesetzt.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Deaktiviere die erneute Abfrage des Master-Passworts, um dieses Feld zu bearbeiten", @@ -2911,10 +3240,18 @@ "message": "Entsperre dein Konto, um passende Zugangsdaten anzuzeigen", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Entsperre dein Konto, um Auto-Ausfüllen-Vorschläge anzuzeigen", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Konto entsperren", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Dein Konto entsperren, öffnet sich in einem neuen Fenster", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Zugangsdaten ausfüllen für", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Neuen Tresor-Eintrag hinzufügen", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "Neue Zugangsdaten", + "description": "Button text to display within inline menu when there are no matching items on a login field" + }, + "addNewLoginItemAria": { + "message": "Neuen Tresor-Zugangsdateneintrag hinzufügen, öffnet sich in einem neuen Fenster", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "Neue Karte", + "description": "Button text to display within inline menu when there are no matching items on a credit card field" + }, + "addNewCardItemAria": { + "message": "Neuen Tresor-Karteneintrag hinzufügen, öffnet sich in einem neuen Fenster", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "Neue Identität", + "description": "Button text to display within inline menu when there are no matching items on an identity field" + }, + "addNewIdentityItemAria": { + "message": "Neuen Tresor-Identitätseintrag hinzufügen, öffnet sich in einem neuen Fenster", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Bitwarden Auto-Ausfüllen Menü verfügbar. Drücke die Pfeiltaste nach unten zum Auswählen.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Fehler beim Verbinden mit dem Duo-Dienst. Verwende eine andere Zwei-Faktor-Authentifizierungsmethode oder kontaktiere Duo für Hilfe." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Starte DUO und folge den Schritten, um die Anmeldung zu abzuschließen." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Ungültiges Dateipasswort. Bitte verwende das Passwort, das du beim Erstellen der Exportdatei eingegeben hast." }, - "importDestination": { - "message": "Import-Ziel" + "destination": { + "message": "Ziel" }, "learnAboutImportOptions": { "message": "Erfahre mehr über deine Importoptionen" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Überprüfung durch die initiierende Website erforderlich. Diese Funktion ist noch nicht für Konten ohne Master-Passwort implementiert." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Mit Passkey anmelden?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "Du hast keinen passenden Zugangsdaten für diese Website." }, + "noMatchingLoginsForSite": { + "message": "Keine passenden Zugangsdaten für diese Seite" + }, "confirm": { "message": "Bestätigen" }, @@ -3143,8 +3510,11 @@ "savePasskeyNewLogin": { "message": "Passkey als neue Zugangsdaten speichern" }, - "choosePasskey": { - "message": "Wähle Zugangsdaten aus, in die dieser Passkey gespeichert werden sollen" + "chooseCipherForPasskeySave": { + "message": "Wähle die Zugangsdaten aus, in die dieser Passkey gespeichert werden soll" + }, + "chooseCipherForPasskeyAuth": { + "message": "Wähle einen Passkey zum Anmelden" }, "passkeyItem": { "message": "Passkey-Eintrag" @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,6 +3659,30 @@ "message": "Gängigste Formate", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Weiter zu den Browsereinstellungen?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Weiter zum Hilfezentrum?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" + }, + "confirmContinueToHelpCenterPasswordManagementContent": { + "message": "Ändere die Auto-Ausfüllen- und Passwort-Verwaltungseinstellungen deines Browsers.", + "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": "Du kannst Tastenkombinationen für Erweiterungen in den Einstellungen deines Browsers anzeigen und festlegen.", + "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": "Ändere die Auto-Ausfüllen- und Passwort-Verwaltungseinstellungen deines Browsers.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" + }, + "confirmContinueToBrowserKeyboardShortcutSettingsContent": { + "message": "Du kannst Tastenkombinationen für Erweiterungen in den Einstellungen deines Browsers anzeigen und festlegen.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Bitwarden zum Standard-Passwort-Manager machen?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" @@ -3317,10 +3711,18 @@ "message": "Zugangsdaten erfolgreich gespeichert!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Passwort gespeichert!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Zugangsdaten erfolgreich aktualisiert!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Passwort aktualisiert!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Fehler beim Speichern der Zugangsdaten. Details in der Konsole.", "description": "Notification message for when saving credentials has failed." @@ -3334,20 +3736,6 @@ "passkeyRemoved": { "message": "Passkey entfernt" }, - "unassignedItemsBannerNotice": { - "message": "Hinweis: Nicht zugeordnete Organisationseinträge sind nicht mehr in der Ansicht aller Tresore sichtbar und nur über die Administrator-Konsole zugänglich." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Hinweis: Ab dem 16. Mai 2024 sind nicht zugewiesene Organisationselemente nicht mehr in der Ansicht aller Tresore sichtbar und nur über die Administrator-Konsole zugänglich." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Weise diese Einträge einer Sammlung aus der", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "zu, um sie sichtbar zu machen.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Vorschläge zum Auto-Ausfüllen" }, @@ -3415,7 +3803,7 @@ }, "autofillTitle": { "message": "Auto-Ausfüllen - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,7 +3814,7 @@ "noValuesToCopy": { "message": "Keine Werte zum Kopieren" }, - "assignCollections": { + "assignToCollections": { "message": "Sammlungen zuweisen" }, "copyEmail": { @@ -3493,13 +3881,13 @@ "message": "Einträge ohne Ordner" }, "itemDetails": { - "message": "Item details" + "message": "Eintrag-Details" }, "itemName": { - "message": "Item name" + "message": "Eintrags-Name" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Du kannst Sammlungen mit Leseberechtigung nicht entfernen: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3899,33 @@ "message": "Organisation ist deaktiviert" }, "owner": { - "message": "Owner" + "message": "Besitzer" }, "selfOwnershipLabel": { - "message": "You", + "message": "Du", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Auf Einträge in deaktivierten Organisationen kann nicht zugegriffen werden. Kontaktiere deinen Organisationseigentümer für Unterstützung." }, + "additionalInformation": { + "message": "Zusätzliche Informationen" + }, + "itemHistory": { + "message": "Eintrags-Verlauf" + }, + "lastEdited": { + "message": "Zuletzt bearbeitet" + }, + "ownerYou": { + "message": "Eigentümer: Du" + }, + "linked": { + "message": "Verknüpft" + }, + "copySuccessful": { + "message": "Erfolgreich kopiert" + }, "upload": { "message": "Hochladen" }, @@ -3530,7 +3936,7 @@ "message": "Die maximale Dateigröße beträgt 500 MB" }, "deleteAttachmentName": { - "message": "Datei $NAME$ löschen", + "message": "Anhang $NAME$ löschen", "placeholders": { "name": { "content": "$1", @@ -3548,15 +3954,389 @@ } }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Sind Sie sich sicher, dass Sie diesen Anhang dauerhaft löschen möchten?" + "message": "Bist du sicher, dass du diesen Anhang dauerhaft löschen möchtest?" }, "premium": { "message": "Premium" }, "freeOrgsCannotUseAttachments": { - "message": "Free organizations cannot use attachments" + "message": "Kostenlose Organisationen können Anhänge nicht verwenden" }, "filters": { "message": "Filter" + }, + "personalDetails": { + "message": "Persönliche Details" + }, + "identification": { + "message": "Identifikation" + }, + "contactInfo": { + "message": "Kontaktinformationen" + }, + "downloadAttachment": { + "message": "Herunterladen - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "Kartennummer endet mit", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Zugangsdaten" + }, + "authenticatorKey": { + "message": "Authenticator-Schlüssel" + }, + "autofillOptions": { + "message": "Auto-Ausfüllen Optionen" + }, + "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 hinzugefügt" + }, + "addWebsite": { + "message": "Website hinzufügen" + }, + "deleteWebsite": { + "message": "Website löschen" + }, + "defaultLabel": { + "message": "Standard ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Übereinstimmungs-Erkennung anzeigen $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Übereinstimmungs-Erkennung verstecken $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Auto-Ausfüllen beim Laden einer Seite?" + }, + "cardExpiredTitle": { + "message": "Abgelaufene Karte" + }, + "cardExpiredMessage": { + "message": "Wenn du die Karte erneuert hast, aktualisiere die Angaben zur Karte" + }, + "cardDetails": { + "message": "Kartendetails" + }, + "cardBrandDetails": { + "message": "$BRAND$ Details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Animationen aktivieren" + }, + "addAccount": { + "message": "Konto hinzufügen" + }, + "loading": { + "message": "Wird geladen" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Nur Organisationsmitglieder mit Zugriff auf diese Sammlungen können die Einträge sehen." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Nur Organisationsmitglieder mit Zugriff auf diese Sammlungen können die Einträge sehen." + }, + "bulkCollectionAssignmentWarning": { + "message": "Du hast $TOTAL_COUNT$ Einträge ausgewählt. Du kannst $READONLY_COUNT$ der Einträge nicht aktualisieren, da du keine Bearbeitungsrechte hast.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Feld hinzufügen" + }, + "add": { + "message": "Hinzufügen" + }, + "fieldType": { + "message": "Feldtyp" + }, + "fieldLabel": { + "message": "Feldbezeichnung" + }, + "textHelpText": { + "message": "Verwende Textfelder für Daten wie Sicherheitsfragen" + }, + "hiddenHelpText": { + "message": "Verwende versteckte Felder für vertrauliche Daten wie ein Passwort" + }, + "checkBoxHelpText": { + "message": "Verwende Kontrollkästchen, wenn du das Kontrollkästchen eines Formulars automatisch ausfüllen möchtest, wie eine Erinnerungs-E-Mail" + }, + "linkedHelpText": { + "message": "Verwende ein verknüpftes Feld, wenn du Probleme mit dem automatischen Ausfüllen einer bestimmten Website hast." + }, + "linkedLabelHelpText": { + "message": "Gib die HTML-ID, den Namen oder das aria-label des Feldes ein. Verwende alternativ einen Platzhalter." + }, + "editField": { + "message": "Feld bearbeiten" + }, + "editFieldLabel": { + "message": "$LABEL$ bearbeiten", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "$LABEL$ löschen", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ hinzugefügt", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "$LABEL$ umsortieren. Verwende die Pfeiltasten, um das Element nach oben oder nach unten zu verschieben.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ nach oben verschoben, Position $INDEX$ von $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Zu zuweisende Sammlungen auswählen" + }, + "personalItemTransferWarningSingular": { + "message": "1 Eintrag wird dauerhaft an die ausgewählte Organisation übertragen. Du wirst diesen Eintrag nicht mehr besitzen." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ werden dauerhaft an die ausgewählte Organisation übertragen. Du wirst diese Einträge nicht mehr besitzen.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 Eintrag wird dauerhaft an $ORG$ übertragen. Du wirst diesen Eintrag nicht mehr besitzen.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ werden dauerhaft an $ORG$ übertragen. Du wirst diese Einträge nicht mehr besitzen.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Sammlungen erfolgreich zugewiesen" + }, + "nothingSelected": { + "message": "Du hast nichts ausgewählt." + }, + "movedItemsToOrg": { + "message": "Ausgewählte Einträge in $ORGNAME$ verschoben", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Einträge verschoben nach $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Eintrag verschoben nach $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ nach unten verschoben, Position $INDEX$ von $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Eintrags-Standort" + }, + "fileSends": { + "message": "Datei-Sends" + }, + "textSends": { + "message": "Text-Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden hat einen neuen Look!" + }, + "bitwardenNewLookDesc": { + "message": "Auto-Ausfüllen und Suchen vom Tresor-Tab ist einfacher und intuitiver als je zuvor. Schau dich um!" + }, + "accountActions": { + "message": "Konto-Aktionen" + }, + "showNumberOfAutofillSuggestions": { + "message": "Anzahl der Vorschläge zum Auto-Ausfüllen von Zugangsdaten auf dem Erweiterungssymbol anzeigen" + }, + "systemDefault": { + "message": "Systemstandard" + }, + "enterprisePolicyRequirementsApplied": { + "message": "Unternehmens-Richtlinienanforderungen wurden auf diese Einstellung angewandt" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Zeichenanzahl anzeigen" + }, + "hideCharacterCount": { + "message": "Zeichenanzahl ausblenden" + }, + "itemsInTrash": { + "message": "Einträge im Papierkorb" + }, + "noItemsInTrash": { + "message": "Keine Einträge im Papierkorb" + }, + "noItemsInTrashDesc": { + "message": "Einträge, die du löschst, erscheinen hier und werden nach 30 Tagen dauerhaft gelöscht" + }, + "trashWarning": { + "message": "Einträge, die länger als 30 Tage im Papierkorb waren, werden automatisch gelöscht" + }, + "restore": { + "message": "Wiederherstellen" + }, + "deleteForever": { + "message": "Dauerhaft löschen" + }, + "noEditPermissions": { + "message": "Du bist nicht berechtigt, diesen Eintrag zu bearbeiten" } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index aff8bb7808d..cc008a4949b 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -3,27 +3,27 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden Διαχειριστής Κωδικών Πρόσβασης", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Το Bitwarden ασφαλίζει παντού και εύκολα τους κωδικούς, τα κλειδιά πρόσβασης και τις ευαίσθητες πληροφορίες σας", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Συνδεθείτε ή δημιουργήστε ένα νέο λογαριασμό για να αποκτήσετε πρόσβαση στο ασφαλές vault σας." }, + "inviteAccepted": { + "message": "Η πρόσκληση έγινε αποδεκτή" + }, "createAccount": { "message": "Δημιουργία λογαριασμού" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Ορίστε έναν ισχυρό κωδικό πρόσβασης" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" - }, - "login": { - "message": "Σύνδεση" + "message": "Ολοκληρώστε τη δημιουργία του λογαριασμού σας ορίζοντας έναν κωδικό πρόσβασης" }, "enterpriseSingleSignOn": { "message": "Ενιαία είσοδος για επιχειρήσεις" @@ -38,10 +38,10 @@ "message": "Υποβολή" }, "emailAddress": { - "message": "Διεύθυνση email" + "message": "Διεύθυνση ηλ. ταχυδρομείου" }, "masterPass": { - "message": "Κύριος κωδικός" + "message": "Κύριος κωδικός πρόσβασης" }, "masterPassDesc": { "message": "Ο κύριος κωδικός είναι ο κωδικός που χρησιμοποιείτε για την πρόσβαση στο vault σας. Είναι πολύ σημαντικό να μην ξεχάσετε τον κύριο κωδικό. Δεν υπάρχει τρόπος να ανακτήσετε τον κωδικό σε περίπτωση που τον ξεχάσετε." @@ -50,7 +50,7 @@ "message": "Η υπόδειξη του κύριου κωδικού μπορεί να σας βοηθήσει να θυμηθείτε τον κωδικό σας, σε περίπτωση που τον ξεχάσετε." }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Αν ξεχάσετε τον κωδικό πρόσβασής σας, η υπενθύμιση του μπορεί να σταλεί στο ηλ. ταχυδρομείο σας. $CURRENT$/$MAXIMUM$ μέγιστοι χαρακτήρες.", "placeholders": { "current": { "content": "$1", @@ -63,10 +63,16 @@ } }, "reTypeMasterPass": { - "message": "Πληκτρολογήστε ξανά τον Κύριο Κωδικό" + "message": "Εισάγετε ξανά τον κύριο κωδικό πρόσβασης" }, "masterPassHint": { - "message": "Υπόδειξη Κύριου Κωδικού (προαιρετικό)" + "message": "Υπόδειξη κύριου κωδικού πρόσβασης (προαιρετικό)" + }, + "joinOrganization": { + "message": "Συμμετοχή στον οργανισμό" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Ολοκληρώστε τη συμμετοχή σας σε αυτόν τον οργανισμό ορίζοντας έναν κύριο κωδικό πρόσβασης." }, "tab": { "message": "Καρτέλα" @@ -75,10 +81,10 @@ "message": "Vault" }, "myVault": { - "message": "Το Vault μου" + "message": "Το θησαυ/κιό μου" }, "allVaults": { - "message": "Όλα τα Vaults" + "message": "Όλα τα θησαυ/κια" }, "tools": { "message": "Εργαλεία" @@ -90,22 +96,37 @@ "message": "Τρέχουσα καρτέλα" }, "copyPassword": { - "message": "Αντιγραφή Κωδικού" + "message": "Αντιγραφή κωδικού πρόσβασης" }, "copyNote": { - "message": "Αντιγραφή Σημείωσης" + "message": "Αντιγραφή σημείωσης" }, "copyUri": { "message": "Αντιγραφή URI" }, "copyUsername": { - "message": "Αντιγραφή Ονόματος Χρήστη" + "message": "Αντιγραφή ονόματος χρήστη" }, "copyNumber": { - "message": "Αντιγραφή Αριθμού" + "message": "Αντιγραφή αριθμού" }, "copySecurityCode": { - "message": "Αντιγραφή Κωδικού Ασφαλείας" + "message": "Αντιγραφή κωδικού ασφαλείας" + }, + "copyName": { + "message": "Αντιγραφή ονόματος" + }, + "copyCompany": { + "message": "Αντιγραφή εταιρείας" + }, + "copySSN": { + "message": "Αντιγραφή ΑΜΚΑ" + }, + "copyPassportNumber": { + "message": "Αντιγραφή αριθμού διαβατηρίου" + }, + "copyLicenseNumber": { + "message": "Αντιγραφή αριθμού άδειας" }, "autoFill": { "message": "Αυτόματη συμπλήρωση" @@ -120,13 +141,13 @@ "message": "Αυτόματη συμπλήρωση ταυτότητας" }, "generatePasswordCopied": { - "message": "Δημιουργία Κωδικού (αντιγράφηκε)" + "message": "Δημιουργία κωδικού πρόσβασης (αντιγράφηκε)" }, "copyElementIdentifier": { "message": "Αντιγραφή ονόματος προσαρμοσμένου πεδίου" }, "noMatchingLogins": { - "message": "Δεν υπάρχουν αντιστοιχίσεις σύνδεσης." + "message": "Καμία σύνδεση δεν ταιριάζει" }, "noCards": { "message": "Δεν υπάρχουν κάρτες" @@ -135,7 +156,7 @@ "message": "Δεν υπάρχουν ταυτότητες" }, "addLoginMenu": { - "message": "Προσθήκη Στοιχείων Σύνδεσης" + "message": "Προσθήκη σύνδεσης" }, "addCardMenu": { "message": "Προσθήκη κάρτας" @@ -150,16 +171,16 @@ "message": "Συνδεθείτε στο vault σας" }, "autoFillInfo": { - "message": "Δεν υπάρχουν διαθέσιμες συνδέσεις για την αυτόματη συμπλήρωση, στην τρέχουσα καρτέλα του προγράμματος περιήγησης." + "message": "Δεν υπάρχουν διαθέσιμες συνδέσεις για αυτόματη συμπλήρωση στην τρέχουσα καρτέλα του περιηγητή." }, "addLogin": { - "message": "Προσθήκη Στοιχείων Σύνδεσης" + "message": "Προσθήκη μίας σύνδεσης" }, "addItem": { - "message": "Προσθήκη Αντικειμένου" + "message": "Προσθήκη αντικειμένου" }, "passwordHint": { - "message": "Υπόδειξη Κωδικού" + "message": "Υπόδειξη κωδικού πρόσβασης" }, "enterEmailToGetHint": { "message": "Εισαγάγετε τη διεύθυνση email του λογαριασμού σας, για να λάβετε την υπόδειξη του κύριου κωδικού." @@ -174,13 +195,13 @@ "message": "Στείλτε έναν κωδικό επαλήθευσης στο email σας" }, "sendCode": { - "message": "Αποστολή Κωδικού" + "message": "Αποστολή κωδικού" }, "codeSent": { "message": "Ο κωδικός στάλθηκε" }, "verificationCode": { - "message": "Κωδικός Επαλήθευσης" + "message": "Κωδικός επαλήθευσης" }, "confirmIdentity": { "message": "Επιβεβαιώστε την ταυτότητα σας για να συνεχίσετε." @@ -189,25 +210,25 @@ "message": "Αλλαγή Κύριου Κωδικού" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Συνέχεια στη διαδικτυακή εφαρμογή;" }, "continueToWebAppDesc": { - "message": "Explore more features of your Bitwarden account on the web app." + "message": "Εξερευνήστε περισσότερες δυνατότητες του λογαριασμού σας Bitwarden στη διαδικτυακή εφαρμογή." }, "continueToHelpCenter": { - "message": "Continue to Help Center?" + "message": "Συνεχίστε στο Κέντρο Βοήθειας;" }, "continueToHelpCenterDesc": { - "message": "Learn more about how to use Bitwarden on the Help Center." + "message": "Μάθετε περισσότερα για το πώς να χρησιμοποιήσετε το Bitwarden στο Κέντρο Βοήθειας." }, "continueToBrowserExtensionStore": { - "message": "Continue to browser extension store?" + "message": "Συνεχίστε στο κατάστημα επεκτάσεων περιηγητή;" }, "continueToBrowserExtensionStoreDesc": { - "message": "Help others find out if Bitwarden is right for them. Visit your browser's extension store and leave a rating now." + "message": "Βοηθήστε άλλους να μάθουν αν το Bitwarden είναι κατάλληλο για αυτούς. Επισκεφθείτε το κατάστημα επεκτάσεων του περιηγητή σας και αφήστε τώρα μια βαθμολογία." }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Μπορείτε να αλλάξετε τον κύριο κωδικό πρόσβασής σας στη διαδικτυακή εφαρμογή του Bitwarden." }, "fingerprintPhrase": { "message": "Φράση Δακτυλικών Αποτυπωμάτων", @@ -224,43 +245,43 @@ "message": "Αποσύνδεση" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "Σχετικά με το Bitwarden" }, "about": { "message": "Σχετικά" }, "moreFromBitwarden": { - "message": "More from Bitwarden" + "message": "Περισσότερα από το Bitwarden" }, "continueToBitwardenDotCom": { - "message": "Continue to bitwarden.com?" + "message": "Συνέχεια στο bitwarden.com;" }, "bitwardenForBusiness": { - "message": "Bitwarden for Business" + "message": "Bitwarden για Επιχειρήσεις" }, "bitwardenAuthenticator": { - "message": "Bitwarden Authenticator" + "message": "Αυθεντικοποιητής 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": "Ο αυθεντικοποιητής Bitwarden σάς επιτρέπει να αποθηκεύετε τα κλειδιά αυθεντικοποίησης και να δημιουργείτε κωδικούς TOTP για ροές επαλήθευσης δύο βημάτων. Μάθετε περισσότερα στην ιστοσελίδα bitwarden.com" }, "bitwardenSecretsManager": { - "message": "Bitwarden Secrets Manager" + "message": "Διαχειριστής Bitwarden Secrets" }, "continueToSecretsManagerPageDesc": { - "message": "Securely store, manage, and share developer secrets with Bitwarden Secrets Manager. Learn more on the bitwarden.com website." + "message": "Αποθηκεύστε και μοιραστείτε μυστικά προγραμματιστών με τον Διαχειριστή Bitwarden Secrets. Μάθετε περισσότερα στον ιστότοπο 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": "Δημιουργήστε ομαλές και ασφαλείς εμπειρίες σύνδεσης δωρεάν από παραδοσιακούς κωδικούς πρόσβασης με το Passwordless.dev. Μάθετε περισσότερα στην ιστοσελίδα bitwarden.com." }, "freeBitwardenFamilies": { - "message": "Free Bitwarden Families" + "message": "Δωρεάν Bitwarden για Οικογένειες" }, "freeBitwardenFamiliesPageDesc": { - "message": "You are eligible for Free Bitwarden Families. Redeem this offer today in the web app." + "message": "Δικαιούστε το Δωρεάν Bitwarden για Οικογένειες. Εξαργυρώστε αυτή την προσφορά σήμερα στην διαδικτυακή εφαρμογή." }, "version": { "message": "Έκδοση" @@ -278,10 +299,28 @@ "message": "Όνομα" }, "editFolder": { - "message": "Επεξεργασία Φακέλου" + "message": "Επεξεργασία φακέλου" + }, + "newFolder": { + "message": "Νέος φάκελος" + }, + "folderName": { + "message": "Όνομα φακέλου" + }, + "folderHintText": { + "message": "Φωλιάστε έναν φάκελο προσθέτοντας το όνομα του γονικού φακέλου ακολουθούμενο από ένα \"/\". Παράδειγμα: Κοινωνικά/Φόρουμ" + }, + "noFoldersAdded": { + "message": "Δεν προστέθηκαν φάκελοι" + }, + "createFoldersToOrganize": { + "message": "Δημιουργήστε φακέλους για να οργανώσετε τα στοιχεία του θησαυ/κίου σας" + }, + "deleteFolderPermanently": { + "message": "Είστε σίγουροι ότι θέλετε να διαγράψετε μόνιμα αυτόν το φάκελο;" }, "deleteFolder": { - "message": "Διαγραφή Φακέλου" + "message": "Διαγραφή φακέλου" }, "folders": { "message": "Φάκελοι" @@ -293,25 +332,25 @@ "message": "Βοήθεια & Σχόλια" }, "helpCenter": { - "message": "Κέντρο βοήθειας Bitwarden" + "message": "Κέντρο Βοήθειας Bitwarden" }, "communityForums": { - "message": "Εξερευνήστε τα φόρουμ της κοινότητας του Bitwarden" + "message": "Εξερευνήστε τα φόρουμ κοινότητας Bitwarden" }, "contactSupport": { - "message": "Επικοινωνία με την υποστήριξη Bitwarden" + "message": "Επικοινωνία με την υποστήριξη του Bitwarden" }, "sync": { "message": "Συγχρονισμός" }, "syncVaultNow": { - "message": "Συγχρονισμός λίστας" + "message": "Συγχρονισμός θησαυ/κιου τώρα" }, "lastSync": { "message": "Τελευταίος συγχρονισμός:" }, "passGen": { - "message": "Γεννήτρια Κωδικού" + "message": "Γεννήτρια κωδικού πρόσβασης" }, "generator": { "message": "Γεννήτρια", @@ -321,7 +360,7 @@ "message": "Δημιουργήστε αυτόματα ισχυρούς και μοναδικούς κωδικούς πρόσβασης για τις συνδέσεις σας." }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "Διαδικτυακή εφαρμογή Bitwarden" }, "importItems": { "message": "Εισαγωγή στοιχείων" @@ -330,10 +369,10 @@ "message": "Επιλογή" }, "generatePassword": { - "message": "Δημιουργία Κωδικού" + "message": "Δημιουργία κωδικού πρόσβασης" }, "regeneratePassword": { - "message": "Επαναδημιουργία Κωδικού" + "message": "Επαναδημιουργία κωδικού πρόσβασης" }, "options": { "message": "Επιλογές" @@ -342,41 +381,86 @@ "message": "Μήκος" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "Ελάχιστο μήκος κωδικού πρόσβασης" }, "uppercase": { - "message": "Κεφαλαία (A-Z)" + "message": "Κεφαλαία (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Πεζά (α-ω)" + "message": "Πεζά (α-ω)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Αριθμοί (0-9)" + "message": "Αριθμοί (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Ειδικοί Χαρακτήρες (!@#$%^&*)" + "message": "Ειδικοί χαρακτήρες (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Συμπερίληψη", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Συμπερίληψη κεφαλαίων χαρακτήρων", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Συμπερίληψη πεζών χαρακτήρων", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Συμπερίληψη αριθμών", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Συμπερίληψη ειδικών χαρακτήρων", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { - "message": "Αριθμός Λέξεων" + "message": "Αριθμός λέξεων" }, "wordSeparator": { - "message": "Διαχωριστής Λέξεων" + "message": "Διαχωριστής λέξεων" }, "capitalize": { "message": "Κεφαλαία", "description": "Make the first letter of a work uppercase." }, "includeNumber": { - "message": "Συμπερίληψη Αριθμών" + "message": "Συμπερίληψη αριθμών" }, "minNumbers": { - "message": "Ελάχιστα Αριθμητικά Ψηφία" + "message": "Ελάχιστα αριθμητικά ψηφία" }, "minSpecial": { - "message": "Ελάχιστο Ειδικών Χαρακτήρων" + "message": "Ελάχιστοι ειδικοί χαρακτήρες" }, "avoidAmbChar": { - "message": "Αποφυγή Αμφιλεγόμενων Χαρακτήρων" + "message": "Αποφυγή αμφιλεγόμενων χαρακτήρων", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Αποφυγή αμφιλεγόμενων χαρακτήρων", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Αναζήτηση στο vault" @@ -391,7 +475,7 @@ "message": "Δεν υπάρχουν στοιχεία στη λίστα." }, "itemInformation": { - "message": "Πληροφορίες Αντικειμένου" + "message": "Πληροφορίες αντικειμένου" }, "username": { "message": "Όνομα Χρήστη" @@ -400,7 +484,7 @@ "message": "Κωδικός" }, "totp": { - "message": "Authenticator secret" + "message": "Μυστικό αυθεντικοποίησης" }, "passphrase": { "message": "Συνθηματικό" @@ -409,13 +493,13 @@ "message": "Αγαπημένο" }, "unfavorite": { - "message": "Unfavorite" + "message": "Αφαίρεση από τα αγαπημένα" }, "itemAddedToFavorites": { - "message": "Item added to favorites" + "message": "Το αντικείμενο προστέθηκε στα αγαπημένα" }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "Το αντικείμενο αφαιρέθηκε από τα αγαπημένα" }, "notes": { "message": "Σημειώσεις" @@ -424,28 +508,28 @@ "message": "Σημείωση" }, "editItem": { - "message": "Επεξεργασία Αντικειμένου" + "message": "Επεξεργασία αντικειμένου" }, "folder": { "message": "Φάκελος" }, "deleteItem": { - "message": "Διαγραφή Στοιχείου" + "message": "Διαγραφή στοιχείου" }, "viewItem": { - "message": "Προβολή Αντικειμένου" + "message": "Προβολή αντικειμένου" }, "launch": { "message": "Εκκίνηση" }, "launchWebsite": { - "message": "Launch website" + "message": "Εκκίνηση ιστοσελίδας" }, "website": { "message": "Ιστοσελίδα" }, "toggleVisibility": { - "message": "Εναλλαγή Ορατότητας" + "message": "Εναλλαγή ορατότητας" }, "manage": { "message": "Διαχείριση" @@ -454,19 +538,19 @@ "message": "Άλλες" }, "unlockMethods": { - "message": "Unlock options" + "message": "Επιλογές ξεκλειδώματος" }, "unlockMethodNeededToChangeTimeoutActionDesc": { - "message": "Ρυθμίστε μια μέθοδο ξεκλειδώματος για να αλλάξετε την ενέργεια χρονικού ορίου θησαυ/κιου." + "message": "Ορίστε μια μέθοδο ξεκλειδώματος για να αλλάξετε την ενέργεια χρονικού ορίου λήξης θησαυ/κίου." }, "unlockMethodNeeded": { - "message": "Set up an unlock method in Settings" + "message": "Ορίστε μία μέθοδο ξεκλειδώματος στις Ρυθμίσεις" }, "sessionTimeoutHeader": { - "message": "Session timeout" + "message": "Χρονικό όριο λήξης συνεδρίας" }, "otherOptions": { - "message": "Other options" + "message": "Άλλες επιλογές" }, "rateExtension": { "message": "Βαθμολογήστε την επέκταση" @@ -509,7 +593,7 @@ "message": "Κλείδωμα Τώρα" }, "lockAll": { - "message": "Lock all" + "message": "Κλείδωμα όλων" }, "immediately": { "message": "Άμεσα" @@ -556,6 +640,18 @@ "security": { "message": "Ασφάλεια" }, + "confirmMasterPassword": { + "message": "Επιβεβαίωση κύριου κωδικού πρόσβασης" + }, + "masterPassword": { + "message": "Κύριος κωδικός πρόσβασης" + }, + "masterPassImportant": { + "message": "Ο κύριος κωδικός πρόσβασής σας δεν μπορεί να ανακτηθεί αν τον ξεχάσετε!" + }, + "masterPassHintLabel": { + "message": "Υπόδειξη κύριου κωδικού πρόσβασης" + }, "errorOccurred": { "message": "Παρουσιάστηκε σφάλμα" }, @@ -572,7 +668,7 @@ "message": "Απαιτείται ξανά ο κύριος κωδικός πρόσβασης." }, "masterPasswordMinlength": { - "message": "Ο κύριος κωδικός πρέπει να έχει μήκος τουλάχιστον $VALUE$ χαρακτήρες.", + "message": "Ο κύριος κωδικός πρόσβασης πρέπει να έχει τουλάχιστον $VALUE$ χαρακτήρες μήκος.", "description": "The Master Password must be at least a specific number of characters long.", "placeholders": { "value": { @@ -587,11 +683,17 @@ "newAccountCreated": { "message": "Ο λογαριασμός σας έχει δημιουργηθεί! Τώρα μπορείτε να συνδεθείτε." }, + "newAccountCreated2": { + "message": "Ο νέος σας λογαριασμός έχει δημιουργηθεί!" + }, + "youHaveBeenLoggedIn": { + "message": "Έχετε συνδεθεί!" + }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Έχετε συνδεθεί επιτυχώς" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Μπορείτε να κλείσετε αυτό το παράθυρο" }, "masterPassSent": { "message": "Σας στείλαμε ένα email με την υπόδειξη του κύριου κωδικού." @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Απαιτείται ο κωδικός επαλήθευσης." }, + "webauthnCancelOrTimeout": { + "message": "Η αυθεντικοποίηση ακυρώθηκε ή διήρκησε πολύ ώρα. Παρακαλώ προσπαθήστε ξανά." + }, "invalidVerificationCode": { "message": "Μη έγκυρος κωδικός επαλήθευσης" }, @@ -613,29 +718,56 @@ } }, "autofillError": { - "message": "Δεν είναι δυνατή η αυτόματη συμπλήρωση του επιλεγμένου στοιχείου σε αυτήν τη σελίδα. Αντιγράψτε και επικολλήστε τις πληροφορίες." + "message": "Δεν είναι δυνατή η αυτόματη συμπλήρωση του επιλεγμένου αντικειμένου σ' αυτήν τη σελίδα. Αντιγράψτε και επικολλήστε τις πληροφορίες." }, "totpCaptureError": { - "message": "Unable to scan QR code from the current webpage" + "message": "Αδυναμία σάρωσης του κωδικού QR από την τρέχουσα ιστοσελίδα" }, "totpCaptureSuccess": { - "message": "Authenticator key added" + "message": "Το κλειδι αυθεντικοποίησης προστέθηκε" }, "totpCapture": { - "message": "Scan authenticator QR code from current webpage" + "message": "Σάρωση κωδικού QR αυθεντικοποιητή από την τρέχουσα ιστοσελίδα" + }, + "totpHelperTitle": { + "message": "Κάντε την επαλήθευση δύο βημάτων απρόσκοπτη" + }, + "totpHelper": { + "message": "Το Bitwarden μπορεί να αποθηκεύσει και να συμπληρώσει τους κωδικούς επαλήθευσης 2 βημάτων. Αντιγράψτε και επικολλήστε το κλειδί σε αυτό το πεδίο." + }, + "totpHelperWithCapture": { + "message": "Το Bitwarden μπορεί να αποθηκεύσει και να συμπληρώσει τους κωδικούς επαλήθευσης 2 βημάτων. Επιλέξτε το εικονίδιο της κάμερας για λήψη στιγμιότυπου του κωδικού QR του αυθεντικοποιητή αυτής της ιστοσελίδας, ή αντιγράψτε και επικολλήστε το κλειδί σε αυτό το πεδίο." + }, + "learnMoreAboutAuthenticators": { + "message": "Μάθετε περισσότερα για της εφαρμογές αυθεντικοποίησης" }, "copyTOTP": { - "message": "Copy Authenticator key (TOTP)" + "message": "Αντιγραφή κλειδιού Αυθεντικοποιητή (TOTP)" }, "loggedOut": { "message": "Αποσυνδεθήκατε" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "Έχετε αποσυνδεθεί από τον λογαριασμό σας." }, "loginExpired": { "message": "Η περίοδος σύνδεσης σας έχει λήξει." }, + "logIn": { + "message": "Σύνδεση" + }, + "restartRegistration": { + "message": "Επανεκκίνηση εγγραφής" + }, + "expiredLink": { + "message": "Ο σύνδεσμος έληξε" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Παρακαλούμε επανακκινήστε την εγγραφή ή δοκιμάστε να συνδεθείτε." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Μπορεί να έχετε ήδη λογαριασμό" + }, "logOutConfirmation": { "message": "Είστε βέβαιοι ότι θέλετε να αποσυνδεθείτε;" }, @@ -655,19 +787,19 @@ "message": "Προστέθηκε φάκελος" }, "twoStepLoginConfirmation": { - "message": "Η σύνδεση σε δύο βήματα καθιστά πιο ασφαλή τον λογαριασμό σας, απαιτώντας να επαληθεύσετε τα στοιχεία σας με μια άλλη συσκευή, όπως κλειδί ασφαλείας, εφαρμογή επαλήθευσης, μήνυμα SMS, τηλεφωνική κλήση ή email. Μπορείτε να ενεργοποιήσετε τη σύνδεση σε δύο βήματα στο bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" + "message": "Η σύνδεση δύο βημάτων καθιστά πιο ασφαλή τον λογαριασμό σας, απαιτώντας να επαληθεύσετε τη συνδεσή σας με μια άλλη συσκευή, όπως ένα κλειδί ασφαλείας, μία εφαρμογή επαλήθευσης, ένα μήνυμα SMS, μία τηλεφωνική κλήση ή ένα μήνυμα ηλ. ταχυδρομείου. Μπορείτε να ενεργοποιήσετε τη σύνδεση δύο βημάτων στο διαδικτυακό θυσαυ/κιο bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" }, "editedFolder": { - "message": "Επεξεργασμένος φάκελος" + "message": "Ο φάκελος αποθηκεύτηκε" }, "deleteFolderConfirmation": { "message": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν το φάκελο;" }, "deletedFolder": { - "message": "Διεγραμμένος φάκελος" + "message": "Ο φάκελος διαγράφηκε" }, "gettingStartedTutorial": { - "message": "Οδηγός Εκμάθησης" + "message": "Οδηγός εκμάθησης" }, "gettingStartedTutorialVideo": { "message": "Παρακολουθήστε τον οδηγό εκμάθησης μας, για να μάθετε πώς μπορείτε να αξιοποιήσετε στο έπακρο την επέκταση του προγράμματος περιήγησης." @@ -697,17 +829,21 @@ "newUri": { "message": "Νέο URI" }, + "addDomain": { + "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": { "message": "Το στοιχείο προστέθηκε" }, "editedItem": { - "message": "Επεξεργασμένο στοιχείο" + "message": "Το αντικείμενο αποθηκεύτηκε" }, "deleteItemConfirmation": { "message": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το στοιχείο;" }, "deletedItem": { - "message": "Διαγραμμένο στοιχείο" + "message": "Το αντικείμενο μετακινήθηκε στον κάδο απορριμάτων" }, "overwritePassword": { "message": "Αντικατάσταση κωδικού πρόσβασης" @@ -737,17 +873,26 @@ "enableAddLoginNotification": { "message": "Ζητήστε να προσθέσετε σύνδεση" }, + "vaultSaveOptionsTitle": { + "message": "Αποθήκευση στις επιλογές θησαυ/κίου" + }, "addLoginNotificationDesc": { "message": "Η \"Προσθήκη Ειδοποίησης Σύνδεσης\" σας προτρέπει αυτόματα να αποθηκεύσετε νέες συνδέσεις στο vault σας κάθε φορά που θα συνδεθείτε για πρώτη φορά." }, "addLoginNotificationDescAlt": { - "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." + "message": "Ζητήστε να προσθέσετε ένα αντικείμενο αν δε βρεθεί στο θησαυ/κιό σας. Ισχύει για όλους τους συνδεδεμένους λογαριασμούς." + }, + "showCardsInVaultView": { + "message": "Εμφάνιση καρτών ως προτάσεις αυτόματης συμπλήρωσης στην προβολή Θησαυ/κίου" }, "showCardsCurrentTab": { "message": "Εμφάνιση καρτών στη σελίδα Καρτέλας" }, "showCardsCurrentTabDesc": { - "message": "Λίστα αντικειμένων καρτών στη σελίδα Καρτέλας για εύκολη αυτόματη συμπλήρωση." + "message": "Εμφάνισε τα αντικείμενα κάρτες στη σελίδα Καρτέλα για εύκολη αυτόματη συμπλήρωση." + }, + "showIdentitiesInVaultView": { + "message": "Εμφάνιση ταυτοτήτων ως προτάσεις αυτόματης συμπλήρωσης στην προβολή Θησαυ/κίου" }, "showIdentitiesCurrentTab": { "message": "Εμφάνιση ταυτοτήτων στη σελίδα καρτέλας" @@ -776,13 +921,13 @@ "message": "Ζητήστε να ενημερώσετε τον κωδικό πρόσβασης μιας σύνδεσης όταν εντοπιστεί μια αλλαγή σε μια ιστοσελίδα." }, "changedPasswordNotificationDescAlt": { - "message": "Ask to update a login's password when a change is detected on a website. Applies to all logged in accounts." + "message": "Ρώτησε για να ενημερώσεις τον κωδικό πρόσβασης μιας σύνδεσης όταν εντοπιστεί μια αλλαγή σε έναν ιστότοπο. Ισχύει για όλους τους συνδεδεμένους λογαριασμούς." }, "enableUsePasskeys": { - "message": "Ask to save and use passkeys" + "message": "Ρώτησε για αποθήκευση και χρήση κλειδιών πρόσβασης" }, "usePasskeysDesc": { - "message": "Ask to save new passkeys or log in with passkeys stored in your vault. Applies to all logged in accounts." + "message": "Ρώτησε με για την αποθήκευση νέων συνθηματικών ή σύνδεση με κλειδιά πρόσβασης αποθηκευμένα στο θησαυ/κιό μου. Ισχύει για όλους τους συνδεδεμένους λογαριασμούς." }, "notificationChangeDesc": { "message": "Θέλετε να ενημερώσετε αυτό τον κωδικό στο Bitwarden ;" @@ -797,7 +942,7 @@ "message": "Ξεκλείδωμα" }, "additionalOptions": { - "message": "Additional options" + "message": "Πρόσθετες επιλογές" }, "enableContextMenuItem": { "message": "Εμφάνιση επιλογών μενού περιβάλλοντος" @@ -806,11 +951,11 @@ "message": "Χρησιμοποιήστε ένα δευτερεύον κλικ για να αποκτήσετε πρόσβαση στη δημιουργία κωδικού πρόσβασης και να ταιριάξετε τις συνδέσεις για την ιστοσελίδα. " }, "contextMenuItemDescAlt": { - "message": "Use a secondary click to access password generation and matching logins for the website. Applies to all logged in accounts." + "message": "Χρησιμοποιήστε ένα δευτερεύον κλικ για να αποκτήσετε πρόσβαση στη δημιουργία κωδικού πρόσβασης και να ταιριάξετε συνδέσεις για την ιστοσελίδα. Ισχύει για όλους τους συνδεδεμένους λογαριασμούς." }, "defaultUriMatchDetection": { "message": "Προεπιλεγμένη ανίχνευση αντιστοιχίας URI", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Επιλέξτε τον προκαθορισμένο τρόπο με τον οποίο αντιμετωπίζεται η ανίχνευση αντιστοίχισης URI για τις συνδέσεις κατά την εκτέλεση ενεργειών όπως η αυτόματη συμπλήρωση." @@ -822,7 +967,7 @@ "message": "Αλλαγή χρώματος θέματος εφαρμογής." }, "themeDescAlt": { - "message": "Change the application's color theme. Applies to all logged in accounts." + "message": "Αλλαγή του χρωματικού θέματος της εφαρμογής. Ισχύει για όλους τους συνδεδεμένους λογαριασμούς." }, "dark": { "message": "Σκοτεινό", @@ -837,44 +982,44 @@ "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, "exportFrom": { - "message": "Export from" + "message": "Εξαγωγή από" }, "exportVault": { "message": "Εξαγωγή Vault" }, "fileFormat": { - "message": "Μορφή αρχείου" + "message": "Τύπος αρχείου" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "Αυτή η εξαγωγή αρχείου θα προστατεύεται με κωδικό πρόσβασης και θα απαιτείται ο κωδικός πρόσβασης του αρχείου για αποκρυπτογράφηση." }, "filePassword": { - "message": "File password" + "message": "Κωδικός πρόσβασης αρχείου" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "Αυτός ο κωδικός πρόσβασης θα χρησιμοποιηθεί για την εξαγωγή και εισαγωγή αυτού του αρχείου" }, "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": "Χρησιμοποιήστε το κλειδί κρυπτογράφησης του λογαριασμού σας, που προέρχεται από το όνομα χρήστη και τον Κύριο Κωδικό Πρόσβασης του λογαριασμού σας, για να κρυπτογραφήσετε την εξαγωγή και να περιορίσετε την εισαγωγή μόνο στον τρέχοντα λογαριασμό Bitwarden." }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "Ορίστε έναν κωδικό πρόσβασης αρχείου για να κρυπτογραφήσετε την εξαγωγή, και να την εισάγετε σε οποιονδήποτε λογαριασμό Bitwarden χρησιμοποιώντας τον κωδικό πρόσβασης για αποκρυπτογράφηση." }, "exportTypeHeading": { - "message": "Export type" + "message": "Τύπος εξαγωγής" }, "accountRestricted": { - "message": "Account restricted" + "message": "Περιορισμένο στο λογαριασμό" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "“Κωδικός πρόσβασης αρχείου” και “Επιβεβαίωση κωδικού πρόσβασης αρχείου“ δεν ταιριάζουν." }, "warning": { "message": "ΠΡΟΕΙΔΟΠΟΙΗΣΗ", "description": "WARNING (should stay in capitalized letters if the language permits)" }, "confirmVaultExport": { - "message": "Επιβεβαίωση εξαγωγής vault" + "message": "Επιβεβαίωση εξαγωγής θησαυ/κίου" }, "exportWarningDesc": { "message": "Αυτή η εξαγωγή περιέχει τα δεδομένα σε μη κρυπτογραφημένη μορφή. Δεν πρέπει να αποθηκεύετε ή να στείλετε το εξαγόμενο αρχείο μέσω μη ασφαλών τρόπων (όπως μέσω email). Διαγράψτε το αμέσως μόλις τελειώσετε με τη χρήση του." @@ -892,7 +1037,7 @@ "message": "Κοινοποιήθηκε" }, "bitwardenForBusinessPageDesc": { - "message": "Bitwarden for Business allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website." + "message": "Το Bitwarden για Επιχειρήσεις σας επιτρέπει να μοιραστείτε τα αντικείμενα του θησαυ/κίου σας με άλλους χρησιμοποιώντας έναν οργανισμό. Μάθετε περισσότερα στην ιστοσελίδα bitwarden.com." }, "moveToOrganization": { "message": "Μετακίνηση σε οργανισμό" @@ -959,10 +1104,10 @@ "message": "Το μέγιστο μέγεθος αρχείου είναι 500 MB." }, "featureUnavailable": { - "message": "Μη διαθέσιμο χαρακτηριστικό" + "message": "Μη διαθέσιμη λειτουργία" }, "encryptionKeyMigrationRequired": { - "message": "Απαιτείται μεταφορά κλειδιού κρυπτογράφησης. Παρακαλούμε συνδεθείτε μέσω του διαδικτυακού θησαυ/κιου για να ενημερώσετε το κλειδί κρυπτογράφησης." + "message": "Απαιτείται μεταφορά κλειδιού κρυπτογράφησης. Παρακαλούμε συνδεθείτε μέσω του διαδικτυακού θησαυ/κίου για να ενημερώσετε το κλειδί κρυπτογράφησης." }, "premiumMembership": { "message": "Συνδρομή Premium" @@ -977,14 +1122,17 @@ "message": "Ανανέωση συνδρομής" }, "premiumNotCurrentMember": { - "message": "Δεν είστε μέλος Premium." + "message": "Δεν είστε Premium μέλος αυτήν τη στιγμή." }, "premiumSignUpAndGet": { - "message": "Εγγραφείτε για συνδρομή Premium και λάβετε:" + "message": "Εγγραφείτε για μία συνδρομή Premium και λάβετε:" }, "ppremiumSignUpStorage": { "message": "1 GB κρυπτογραφημένο αποθηκευτικό χώρο για συνημμένα αρχεία." }, + "premiumSignUpEmergency": { + "message": "Πρόσβαση έκτακτης ανάγκης." + }, "premiumSignUpTwoStepOptions": { "message": "Πρόσθετες επιλογές σύνδεσης δύο βημάτων, όπως το YubiKey και το Duo." }, @@ -998,20 +1146,26 @@ "message": "Προτεραιότητα υποστήριξης πελατών." }, "ppremiumSignUpFuture": { - "message": "Όλα τα μελλοντικά χαρακτηριστικά Premium. Έρχονται περισσότερα σύντομα!" + "message": "Όλες οι μελλοντικές λειτουργίες Premium. Έρχονται περισσότερα σύντομα!" }, "premiumPurchase": { "message": "Αγορά Premium έκδοσης" }, "premiumPurchaseAlert": { - "message": "Μπορείτε να αγοράσετε συνδρομή Premium στο web vault του bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" + "message": "Μπορείτε να αγοράσετε συνδρομή Premium στο διαδικτυακό θησαυ/κιο του bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" + }, + "premiumPurchaseAlertV2": { + "message": "Μπορείτε να αγοράσετε το Premium από τις ρυθμίσεις του λογαριασμού σας στην διαδικτυακή εφαρμογή Bitwarden." }, "premiumCurrentMember": { - "message": "Είστε μέλος Premium!" + "message": "Είστε Premium μέλος!" }, "premiumCurrentMemberThanks": { "message": "Ευχαριστούμε που υποστηρίζετε το Bitwarden." }, + "premiumFeatures": { + "message": "Αναβαθμίστε σε Premium και λάβετε:" + }, "premiumPrice": { "message": "Όλα για μόνο $PRICE$ /έτος!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "Όλα μόνο για $PRICE$ ανά έτος!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Επιτυχής ανανέωση" }, @@ -1034,10 +1197,10 @@ "message": "Ζητήστε βιομετρικά κατά την εκκίνηση" }, "premiumRequired": { - "message": "Απαιτείται έκδοση Premium" + "message": "Απαιτείται το Premium" }, "premiumRequiredDesc": { - "message": "Για να χρησιμοποιήσετε αυτή τη λειτουργία, απαιτείται έκδοση Premium." + "message": "Για να χρησιμοποιήσετε αυτή τη λειτουργία, απαιτείται συνδρομή Premium." }, "enterVerificationCodeApp": { "message": "Εισάγετε τον 6ψήφιο κωδικό από την εφαρμογή επαλήθευσης." @@ -1103,20 +1266,20 @@ "message": "Κωδικός ανάκτησης" }, "authenticatorAppTitle": { - "message": "Εφαρμογή ελέγχου ταυτότητας" + "message": "Εφαρμογή αυθεντικοποίησης" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Εισάγετε έναν κωδικό που δημιουργήθηκε από μια εφαρμογή αυθεντικοποίησης όπως το Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP Security Key" + "message": "Κλειδί Ασφαλείας Yubico OTP" }, "yubiKeyDesc": { "message": "Χρησιμοποιήστε ένα YubiKey για να αποκτήσετε πρόσβαση στο λογαριασμό σας. Λειτουργεί με συσκευές σειράς YubiKey 4, 4 Nano, 4C και συσκευές NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Εισάγετε έναν κωδικό που δημιουργήθηκε από το Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1127,13 +1290,13 @@ "message": "FIDO2 WebAuthn" }, "webAuthnDesc": { - "message": "Χρησιμοποιήστε οποιοδήποτε κλειδί ασφαλείας συμβατό με το WebAuthn για να αποκτήσετε πρόσβαση στο λογαριασμό σας." + "message": "Χρησιμοποιήστε οποιοδήποτε κλειδί ασφαλείας συμβατό με το WebAuthn για να αποκτήσετε πρόσβαση στον λογαριασμό σας." }, "emailTitle": { "message": "Email" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Εισάγετε τον κωδικό που σας στάλθηκε στο ηλ. ταχυδρομείο." }, "selfHostedEnvironment": { "message": "Αυτο-φιλοξενούμενο περιβάλλον" @@ -1142,13 +1305,13 @@ "message": "Καθορίστε τη βασική διεύθυνση URL, της εγκατάστασης του Bitwarden που φιλοξενείται στο χώρο σας." }, "selfHostedBaseUrlHint": { - "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" + "message": "Καθορίστε το βασικό URL της εγκατάστασης Bitwarden στο χώρο σας. Παράδειγμα: https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { - "message": "For advanced configuration, you can specify the base URL of each service independently." + "message": "Για προχωρημένη παραμετροποίηση, μπορείτε να ορίσετε ανεξάρτητα το βασικό URL κάθε υπηρεσίας." }, "selfHostedEnvFormInvalid": { - "message": "You must add either the base Server URL or at least one custom environment." + "message": "Πρέπει να προσθέσετε είτε το βασικό URL του διακομιστή ή τουλάχιστον ένα προσαρμοσμένο περιβάλλον." }, "customEnvironment": { "message": "Προσαρμοσμένο περιβάλλον" @@ -1160,57 +1323,85 @@ "message": "URL Διακομιστή" }, "apiUrl": { - "message": "URL Διακομιστή API" + "message": "URL διακομιστή API" }, "webVaultUrl": { - "message": "Web Vault Server URL" + "message": "URL διακομιστή διαδικτυακού θησαυ/κίου" }, "identityUrl": { - "message": "URL Ταυτότητας Διακομιστή" + "message": "URL διακομιστή ταυτότητας" }, "notificationsUrl": { - "message": "Ειδοποιήσεις Διεύθυνσης URL Διακομιστή" + "message": "URL διακομιστή ειδοποιήσεων" }, "iconsUrl": { - "message": "Εικονίδια διακομιστή URL" + "message": "URL διακομιστή εικονιδίων" }, "environmentSaved": { - "message": "Οι διευθύνσεις URL περιβάλλοντος έχουν αποθηκευτεί." + "message": "Τα URL περιβάλλοντος έχουν αποθηκευτεί" }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Εμφάνιση μενού αυτόματης συμπλήρωσης στα πεδία της φόρμας", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { - "message": "Applies to all logged in accounts." + "autofillSuggestionsSectionTitle": { + "message": "Πρόταση αυτόματης συμπλήρωσης" + }, + "showInlineMenuLabel": { + "message": "Εμφάνιση μενού αυτόματης συμπλήρωσης στα πεδία της φόρμας" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Εμφάνιση προτάσεων όταν το εικονίδιο είναι επιλεγμένο" + }, + "showInlineMenuOnFormFieldsDescAlt": { + "message": "Ισχύει για όλους τους συνδεδεμένους λογαριασμούς." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Απενεργοποιήστε την ρύθμιση του διαχειριστή κωδικών πρόσβασης του περιηγητή σας για να αποφύγετε συγκρούσεις." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { - "message": "Edit browser settings." + "message": "Επεξεργαστείτε τις ρυθμίσεις του περιηγητή." }, "autofillOverlayVisibilityOff": { - "message": "Off", + "message": "Ανενεργό", "description": "Overlay setting select option for disabling autofill overlay" }, "autofillOverlayVisibilityOnFieldFocus": { - "message": "When field is selected (on focus)", + "message": "Όταν το πεδίο είναι επιλεγμένο (σε εστίαση)", "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "Όταν το εικονίδιο αυτόματης συμπλήρωσης είναι επιλεγμένο", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Αυτόματη συμπλήρωση κατά την φόρτωση της σελίδας" + }, "enableAutoFillOnPageLoad": { - "message": "Ενεργοποίηση αυτόματης συμπλήρωσης κατά την φόρτωση της σελίδας" + "message": "Αυτόματη συμπλήρωση κατά τη φόρτωση της σελίδας" }, "enableAutoFillOnPageLoadDesc": { - "message": "Εάν εντοπιστεί μια φόρμα σύνδεσης, πραγματοποιείται αυτόματα μια αυτόματη συμπλήρωση όταν φορτώνεται η ιστοσελίδα." + "message": "Εάν εντοπιστεί φόρμα σύνδεσης, θα συμπληρωθεί αυτόματα κατά τη φόρτωση της σελίδας." + }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Προειδοποίηση:$CLOSETAG$ Οι παραβιασμένες ή μη αξιόπιστες ιστοσελίδες μπορούν να εκμεταλλευτούν την αυτόματη συμπλήρωση κατά τη φόρτωση της σελίδας.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } }, "experimentalFeature": { "message": "Παραβιασμένοι ή μη αξιόπιστοι ιστότοποι μπορούν να εκμεταλλευτούν την αυτόματη συμπλήρωση κατά τη φόρτωση της σελίδας." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Μάθετε περισσότερα σχετικά με τους κινδύνους" + }, "learnMoreAboutAutofill": { "message": "Μάθετε περισσότερα σχετικά με την αυτόματη συμπλήρωση" }, @@ -1218,19 +1409,19 @@ "message": "Προεπιλεγμένη ρύθμιση αυτόματης συμπλήρωσης για στοιχεία σύνδεσης" }, "defaultAutoFillOnPageLoadDesc": { - "message": "Μπορείτε να απενεργοποιήσετε την αυτόματη συμπλήρωση φόρτωσης σελίδας για μεμονωμένα στοιχεία σύνδεσης από την προβολή Επεξεργασία στοιχείου." + "message": "Μπορείτε να απενεργοποιήσετε την αυτόματη συμπλήρωση κατά τη φόρτωση της σελίδας για μεμονωμένα αντικείμενα σύνδεσης από την προβολή Επεξεργασία αντικειμένου." }, "itemAutoFillOnPageLoad": { - "message": "Αυτόματη συμπλήρωση κατά τη φόρτωση της σελίδας (αν έχει ενεργοποιηθεί στις Ρυθμίσεις)" + "message": "Αυτόματη συμπλήρωση κατά τη φόρτωση της σελίδας (αν έχει ενεργοποιηθεί στις Επιλογές)" }, "autoFillOnPageLoadUseDefault": { "message": "Χρήση προεπιλεγμένης ρύθμισης" }, "autoFillOnPageLoadYes": { - "message": "Αυτόματη συμπλήρωση κατά την φόρτωση της σελίδας" + "message": "Αυτόματη συμπλήρωση κατά τη φόρτωση της σελίδας" }, "autoFillOnPageLoadNo": { - "message": "Μη αυτόματη συμπλήρωση κατά τη φόρτωση της σελίδας" + "message": "Μη συμπληρώνεις αυτόματα κατά τη φόρτωση της σελίδας" }, "commandOpenPopup": { "message": "Άνοιγμα αναδυόμενου vault" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Άνοιγμα αναδυόμενου vault στην πλευρική μπάρα" }, - "commandAutofillDesc": { - "message": "Αυτόματη συμπλήρωση της τελευταίας χρησιμοποιούμενης σύνδεσης για αυτόν τον ιστότοπο" + "commandAutofillLoginDesc": { + "message": "Αυτόματη συμπλήρωση της τελευταίας σύνδεσης που χρησιμοποιήθηκε για τον τρέχοντα ιστότοπο" + }, + "commandAutofillCardDesc": { + "message": "Αυτόματη συμπλήρωση της τελευταίας κάρτας που χρησιμοποιήθηκε για τον τρέχοντα ιστότοπο" + }, + "commandAutofillIdentityDesc": { + "message": "Αυτόματη συμπλήρωση της τελευταίας ταυτότητας που χρησιμοποιήθηκε για τον τρέχοντα ιστότοπο" }, "commandGeneratePasswordDesc": { "message": "Δημιουργήστε και αντιγράψτε έναν νέο τυχαίο κωδικό πρόσβασης στο πρόχειρο" @@ -1248,16 +1445,16 @@ "message": "Κλειδώστε το vault" }, "customFields": { - "message": "Προσαρμοσμένα Πεδία" + "message": "Προσαρμοσμένα πεδία" }, "copyValue": { - "message": "Αντιγραφή Τιμής" + "message": "Αντιγραφή τιμής" }, "value": { "message": "Τιμή" }, "newCustomField": { - "message": "Νέο Προσαρμοσμένο Πεδίο" + "message": "Νέο προσαρμοσμένο πεδίο" }, "dragToSort": { "message": "Σύρετε για ταξινόμηση" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Δυαδικό" }, + "cfTypeCheckbox": { + "message": "Πλαίσιο επιλογής" + }, "cfTypeLinked": { "message": "Συνδεδεμένο", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1292,7 +1492,7 @@ "message": "Εμφάνιση μιας αναγνωρίσιμης εικόνας δίπλα σε κάθε σύνδεση." }, "faviconDescAlt": { - "message": "Show a recognizable image next to each login. Applies to all logged in accounts." + "message": "Εμφάνιση μιας αναγνωρίσιμης εικόνας δίπλα σε κάθε σύνδεση. Ισχύει για όλους τους συνδεδεμένους λογαριασμούς." }, "enableBadgeCounter": { "message": "Εμφάνιση μετρητή εμβλημάτων" @@ -1301,7 +1501,7 @@ "message": "Υποδείξτε πόσες συνδέσεις έχετε για την τρέχουσα ιστοσελίδα." }, "cardholderName": { - "message": "Όνομα κατόχου της κάρτας" + "message": "Όνομα κατόχου κάρτας" }, "number": { "message": "Αριθμός" @@ -1310,7 +1510,7 @@ "message": "Επωνυμία" }, "expirationMonth": { - "message": "Μήνας Λήξης" + "message": "Μήνας λήξης" }, "expirationYear": { "message": "Έτος λήξης" @@ -1355,7 +1555,7 @@ "message": "Δεκέμβριος" }, "securityCode": { - "message": "Κωδικός Ασφαλείας" + "message": "Κωδικός ασφαλείας" }, "ex": { "message": "πχ." @@ -1376,7 +1576,7 @@ "message": "Dr" }, "mx": { - "message": "Κ@" + "message": "Κ" }, "firstName": { "message": "Όνομα" @@ -1391,7 +1591,7 @@ "message": "Ονοματεπώνυμο" }, "identityName": { - "message": "Όνομα Ταυτότητας" + "message": "Όνομα ταυτότητας" }, "company": { "message": "Εταιρεία" @@ -1400,10 +1600,10 @@ "message": "ΑΜΚΑ" }, "passportNumber": { - "message": "Αριθμός Διαβατηρίου" + "message": "Αριθμός διαβατηρίου" }, "licenseNumber": { - "message": "Αριθμός Άδειας" + "message": "Αριθμός άδειας" }, "email": { "message": "Email" @@ -1430,7 +1630,7 @@ "message": "Περιοχή / Νομός" }, "zipPostalCode": { - "message": "Ταχυδρομικός Κώδικας" + "message": "Ταχυδρομικός κώδικας" }, "country": { "message": "Χώρα" @@ -1445,7 +1645,7 @@ "message": "Συνδέσεις" }, "typeSecureNote": { - "message": "Ασφαλής Σημείωση" + "message": "Ασφαλής σημείωση" }, "typeCard": { "message": "Κάρτα" @@ -1454,7 +1654,7 @@ "message": "Ταυτότητα" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "Νέα $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1463,7 +1663,16 @@ } }, "editItemHeader": { - "message": "Edit $TYPE$", + "message": "Επεξεργασία $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, + "viewItemHeader": { + "message": "Εμφάνιση $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1472,7 +1681,7 @@ } }, "passwordHistory": { - "message": "Ιστορικό Κωδικού" + "message": "Ιστορικό κωδικού πρόσβασης" }, "back": { "message": "Πίσω" @@ -1481,7 +1690,7 @@ "message": "Συλλογές" }, "nCollections": { - "message": "$COUNT$ collections", + "message": "$COUNT$ συλλογές", "placeholders": { "count": { "content": "$1", @@ -1508,7 +1717,7 @@ "message": "Συνδέσεις" }, "secureNotes": { - "message": "Ασφαλείς Σημειώσεις" + "message": "Ασφαλείς σημειώσεις" }, "clear": { "message": "Εκκαθάριση", @@ -1533,6 +1742,10 @@ "message": "Βασικός τομέας", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Βασικός τομέας (συνιστάται)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Όνομα τομέα", "description": "Domain name. Ex. website.com" @@ -1552,15 +1765,15 @@ "description": "A programming term, also known as 'RegEx'." }, "matchDetection": { - "message": "Εντοπισμός Αντιστοίχισης", - "description": "URI match detection for auto-fill." + "message": "Εντοπισμός αντιστοίχισης", + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Προεπιλεγμένος εντοπισμός αντιστοίχισης", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { - "message": "Επιλογές Εναλλαγής" + "message": "Επιλογές εναλλαγής" }, "toggleCurrentUris": { "message": "Εναλλαγή τρεχόντων URI", @@ -1578,11 +1791,20 @@ "message": "Τύποι" }, "allItems": { - "message": "Όλα τα στοιχεία" + "message": "Όλα τα αντικείμενα" }, "noPasswordsInList": { "message": "Δεν υπάρχουν κωδικοί στη λίστα." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Αφαίρεση" }, @@ -1598,7 +1820,7 @@ "description": "ex. Date this item was created" }, "datePasswordUpdated": { - "message": "Ο Κωδικός Ενημερώθηκε", + "message": "Ο κωδικός πρόσβασης ενημερώθηκε", "description": "ex. Date this password was updated" }, "neverLockWarning": { @@ -1651,7 +1873,7 @@ "message": "Μη έγκυρος κωδικός PIN." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "Πάρα πολλές άκυρες απόπειρες εισαγωγής PIN. Γίνεται αποσύνδεση." }, "unlockWithBiometrics": { "message": "Ξεκλείδωμα με βιομετρικά στοιχεία" @@ -1660,7 +1882,7 @@ "message": "Αναμονή επιβεβαίωσης από την επιφάνεια εργασίας" }, "awaitDesktopDesc": { - "message": "Παρακαλώ επιβεβαιώστε τη χρήση βιομετρικών στοιχείων στην εφαρμογή Bitwarden Desktop για να ενεργοποιήσετε τα βιομετρικά στοιχεία για το πρόγραμμα περιήγησης." + "message": "Παρακαλώ επιβεβαιώστε τη χρήση βιομετρικών στην εφαρμογή Bitwarden της επιφάνειας εργασίας για να ενεργοποιήσετε τα βιομετρικά για τον περιηγητή." }, "lockWithMasterPassOnRestart": { "message": "Κλείδωμα με κύριο κωδικό πρόσβασης στην επανεκκίνηση του προγράμματος περιήγησης" @@ -1669,7 +1891,7 @@ "message": "Πρέπει να επιλέξετε τουλάχιστον μία συλλογή." }, "cloneItem": { - "message": "Κλώνος Αντικειμένου" + "message": "Κλωνοποίηση αντικειμένου" }, "clone": { "message": "Κλώνος" @@ -1677,6 +1899,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": "Ενέργεια Χρόνου Λήξης Vault" }, @@ -1698,34 +1943,34 @@ "message": "Είστε βέβαιοι ότι θέλετε να διαγράψετε μόνιμα αυτό το στοιχείο;" }, "permanentlyDeletedItem": { - "message": "Μόνιμα Διεγραμμένο Στοιχείο" + "message": "Το αντικείμενο διαγράφηκε οριστικά" }, "restoreItem": { - "message": "Ανάκτηση Στοιχείου" + "message": "Επαναφορά αντικειμένου" }, "restoredItem": { - "message": "Στοιχείο που έχει Ανακτηθεί" + "message": "Το αντικείμενο επαναφέρθηκε" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Έχετε ήδη λογαριασμό;" }, "vaultTimeoutLogOutConfirmation": { "message": "Η αποσύνδεση θα καταργήσει όλη την πρόσβαση στο vault σας και απαιτεί online έλεγχο ταυτότητας μετά το χρονικό όριο λήξης. Είστε βέβαιοι ότι θέλετε να χρησιμοποιήσετε αυτήν τη ρύθμιση;" }, "vaultTimeoutLogOutConfirmationTitle": { - "message": "Επιβεβαίωση Ενέργειας Χρονικού Ορίου" + "message": "Επιβεβαίωση ενέργειας χρονικού ορίου λήξης" }, "autoFillAndSave": { "message": "Αυτόματη συμπλήρωση και αποθήκευση" }, "fillAndSave": { - "message": "Fill and save" + "message": "Συμπλήρωση και αποθήκευση" }, "autoFillSuccessAndSavedUri": { - "message": "Αυτόματη συμπλήρωση στοιχείου και αποθηκευμένο URI" + "message": "Το αντικείμενο συμπληρώθηκε αυτόματα και το URI αποθηκεύτηκε" }, "autoFillSuccess": { - "message": "Αυτόματη συμπλήρωση αντικειμένου" + "message": "Το αντικείμενο συμπληρώθηκε αυτόματα " }, "insecurePageWarning": { "message": "Προειδοποίηση: Αυτή είναι μια μη ασφαλή σελίδα HTTP και οποιαδήποτε πληροφορία υποβάλλετε μπορεί να γίνει ορατή και επεμβάσιμη από άλλους. Αυτή η σύνδεση αποθηκεύτηκε αρχικά σε μια ασφαλή (HTTPS) σελίδα." @@ -1746,16 +1991,16 @@ } }, "setMasterPassword": { - "message": "Καθορισμός κύριου κωδικού" + "message": "Ορισμός κύριου κωδικού πρόσβασης" }, "currentMasterPass": { - "message": "Τρέχων Κύριος Κωδικός" + "message": "Τρέχων κύριος κωδικός πρόσβασης" }, "newMasterPass": { - "message": "Νέος κύριος κωδικός" + "message": "Νέος κύριος κωδικός πρόσβασης" }, "confirmNewMasterPass": { - "message": "Επιβεβαίωση Νέου Κύριου Κωδικού" + "message": "Επιβεβαίωση νέου κύριου κωδικού πρόσβασης" }, "masterPasswordPolicyInEffect": { "message": "Σε μία ή περισσότερες πολιτικές του οργανισμού απαιτείται ο κύριος κωδικός να πληρεί τις ακόλουθες απαιτήσεις:" @@ -1799,20 +2044,20 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Ο νέος κύριος κωδικός δεν πληροί τις απαιτήσεις πολιτικής." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Λάβετε συμβουλές, ανακοινώσεις και ευκαιρίες έρευνας από το Bitwarden στα εισερχόμενά σας." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "Ακύρωση συνδρομής" }, "atAnyTime": { - "message": "at any time." + "message": "οποιαδήποτε στιγμή." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Συνεχίζοντας, συμφωνείτε με" }, "and": { - "message": "and" + "message": "και" }, "acceptPolicies": { "message": "Επιλέγοντας αυτό το πλαίσιο, συμφωνείτε με τα εξής:" @@ -1833,10 +2078,10 @@ "message": "Οκ" }, "errorRefreshingAccessToken": { - "message": "Access Token Refresh Error" + "message": "Σφάλμα Ανανέωσης Διακριτικού Πρόσβασης" }, "errorRefreshingAccessTokenDesc": { - "message": "No refresh token or API keys found. Please try logging out and logging back in." + "message": "Δε βρέθηκε κανένα διακριτικό ανανέωσης ή κλειδιά API. Παρακαλώ δοκιμάστε να αποσυνδεθείτε και να συνδεθείτε ξανά." }, "desktopSyncVerificationTitle": { "message": "Επιβεβαίωση συγχρονισμού επιφάνειας εργασίας" @@ -1845,10 +2090,10 @@ "message": "Παρακαλώ επιβεβαιώστε ότι η εφαρμογή επιφάνειας εργασίας εμφανίζει αυτό το αποτύπωμα: " }, "desktopIntegrationDisabledTitle": { - "message": "Η ενσωμάτωση του περιηγητή δεν είναι ενεργοποιημένη" + "message": "Η ενσωμάτωση περιηγητή δεν έχει οριστεί" }, "desktopIntegrationDisabledDesc": { - "message": "Η ενσωμάτωση του προγράμματος περιήγησης δεν είναι ενεργοποιημένη στην εφαρμογή Bitwarden Desktop. Παρακαλώ ενεργοποιήστε την στις ρυθμίσεις της εφαρμογής desktop." + "message": "Η ενσωμάτωση περιηγητή δεν έχει οριστεί στην εφαρμογή Bitwarden Desktop. Παρακαλώ ορίστε την στις ρυθμίσεις της εφαρμογής desktop." }, "startDesktopTitle": { "message": "Ξεκινήστε την εφαρμογή Bitwarden Επιφάνεια εργασίας" @@ -1874,8 +2119,14 @@ "nativeMessagingWrongUserTitle": { "message": "Απόρριψη λογαριασμού" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Το βιομετρικό ξεκλείδωμα απέτυχε. Το βιομετρικό μυστικό κλειδί απέτυχε να ξεκλειδώσει το θησαυ/κιο. Προσπαθήστε να ρυθμίσετε ξανά τα βιομετρικά." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Αναντιστοιχία βιομετρικού κλειδιού" + }, "biometricsNotEnabledTitle": { - "message": "Η βιομετρική δεν είναι ενεργοποιημένη" + "message": "Δεν έχουν οριστεί βιομετρικά" }, "biometricsNotEnabledDesc": { "message": "Τα βιομετρικά στοιχεία του προγράμματος περιήγησης απαιτούν την ενεργοποίηση της βιομετρικής επιφάνειας εργασίας στις ρυθμίσεις πρώτα." @@ -1887,13 +2138,19 @@ "message": "Τα βιομετρικά στοιχεία του προγράμματος περιήγησης δεν υποστηρίζονται σε αυτήν τη συσκευή." }, "biometricsNotUnlockedTitle": { - "message": "User locked or logged out" + "message": "Ο χρήστης κλειδώθηκε ή αποσυνδέθηκε" }, "biometricsNotUnlockedDesc": { - "message": "Please unlock this user in the desktop application and try again." + "message": "Παρακαλώ ξεκλειδώστε αυτόν τον χρήστη στην εφαρμογή επιφάνειας εργασίας και προσπαθήστε ξανά." + }, + "biometricsNotAvailableTitle": { + "message": "Μη διαθέσιμο βιομετρικό ξεκλείδωμα" + }, + "biometricsNotAvailableDesc": { + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο προς το παρόν. Παρακαλώ προσπαθήστε ξανά αργότερα." }, "biometricsFailedTitle": { - "message": "Ο βιομετρικός έλεγχος απέτυχε" + "message": "Τα βιομετρικά απέτυχαν" }, "biometricsFailedDesc": { "message": "Τα βιομετρικά δεν μπόρεσαν να ολοκληρωθούν, σκεφτείτε να χρησιμοποιήσετε έναν κύριο κωδικό πρόσβασης ή να αποσυνδεθείτε. Αν αυτό εξακολουθεί να συμβαίνει, παρακαλώ επικοινωνήστε με την υποστήριξη της Bitwarden." @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "Μια οργανωτική πολιτική έχει αποτρέψει την εισαγωγή στοιχείων στο προσωπικό θησαυ/κιο σας." }, + "domainsTitle": { + "message": "Τομείς", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Εξαιρούμενοι Τομείς" }, @@ -1926,17 +2187,29 @@ "message": "Το Bitwarden δεν θα ζητήσει να αποθηκεύσετε τα στοιχεία σύνδεσης για αυτούς τους τομείς. Πρέπει να ανανεώσετε τη σελίδα για να τεθούν σε ισχύ οι αλλαγές." }, "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." + "message": "Το Bitwarden δε θα ρωτήσει για να αποθηκεύσετε τα στοιχεία σύνδεσης για αυτούς τους τομείς, για όλους τους συνδεδεμένους λογαριασμούς. Πρέπει να ανανεώσετε τη σελίδα για να τεθούν σε ισχύ οι αλλαγές." + }, + "websiteItemLabel": { + "message": "Ιστοσελίδα $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } }, "excludedDomainsInvalidDomain": { "message": "Το $DOMAIN$ δεν είναι έγκυρος τομέας", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Οι αλλαγές αποκλεισμένων τομέων αποθηκεύτηκαν" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Προστατευμένο με κωδικό" }, + "copyLink": { + "message": "Αντιγραφή συνδέσμου" + }, "copySendLink": { "message": "Αντιγραφή συνδέσμου Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1983,10 +2259,10 @@ "message": "Διαγραφή" }, "removedPassword": { - "message": "Καταργήθηκε ο Κωδικός Πρόσβασης" + "message": "Ο κωδικός πρόσβασης αφαιρέθηκε" }, "deletedSend": { - "message": "Το Send Διαγράφηκε", + "message": "Το Send διαγράφηκε", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLink": { @@ -2023,14 +2299,14 @@ "message": "Το αρχείο που θέλετε να στείλετε." }, "deletionDate": { - "message": "Ημερομηνία Διαγραφής" + "message": "Ημερομηνία διαγραφής" }, "deletionDateDesc": { "message": "Το Send θα διαγραφεί οριστικά την καθορισμένη ημερομηνία και ώρα.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "expirationDate": { - "message": "Ημερομηνία Λήξης" + "message": "Ημερομηνία λήξης" }, "expirationDateDesc": { "message": "Εάν οριστεί, η πρόσβαση σε αυτό το Send θα λήξει την καθορισμένη ημερομηνία και ώρα.", @@ -2082,17 +2358,17 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "currentAccessCount": { - "message": "Τρέχων Αριθμός Πρόσβασης" + "message": "Τρέχων αριθμός πρόσβασης" }, "createSend": { - "message": "Δημιουργία Νέου Send", + "message": "Νέο Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "newPassword": { - "message": "Νέος Κωδικός Πρόσβασης" + "message": "Νέος κωδικός πρόσβασης" }, "sendDisabled": { - "message": "Το Send Απενεργοποιήθηκε", + "message": "Το Send αφαιρέθηκε", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDisabledWarning": { @@ -2100,11 +2376,29 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSend": { - "message": "Το Send Δημιουργήθηκε", + "message": "Το Send δημιουργήθηκε", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "createdSendSuccessfully": { + "message": "Το Send δημιουργήθηκε επιτυχώς!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "Το Send θα είναι διαθέσιμο σε όποιον έχει τον σύνδεσμο για τις επόμενες $DAYS$ ημέρες.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Ο σύνδεσμος Send αντιγράφηκε", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editedSend": { - "message": "Το Send Επεξεργάστηκε", + "message": "Το Send αποθηκεύτηκε", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLinuxChromiumFileWarning": { @@ -2162,7 +2456,10 @@ "message": "Αυτή η ενέργεια προστατεύεται. Για να συνεχίσετε, πληκτρολογήστε ξανά τον κύριο κωδικό πρόσβασης για να επαληθεύσετε την ταυτότητά σας." }, "emailVerificationRequired": { - "message": "Απαιτείται Επαλήθευση Email" + "message": "Απαιτείται επαλήθευση διεύθυνσης ηλ. ταχυδρομείου" + }, + "emailVerifiedV2": { + "message": "Η διεύθυνση ηλ. ταχυδρομείου επιβεβαιώθηκε" }, "emailVerificationRequiredDesc": { "message": "Πρέπει να επαληθεύσετε το email σας για να χρησιμοποιήσετε αυτή τη δυνατότητα. Μπορείτε να επαληθεύσετε το email σας στο web vault." @@ -2171,7 +2468,7 @@ "message": "Ενημερώθηκε ο κύριος κωδικός πρόσβασης" }, "updateMasterPassword": { - "message": "Ενημερώστε τον κύριο κωδικό πρόσβασης" + "message": "Ενημέρωση κύριου κωδικού πρόσβασης" }, "updateMasterPasswordWarning": { "message": "Ο Κύριος Κωδικός Πρόσβασής σας άλλαξε πρόσφατα από διαχειριστή στον οργανισμό σας. Για να αποκτήσετε πρόσβαση στο vault, πρέπει να τον ενημερώσετε τώρα. Η διαδικασία θα σας αποσυνδέσει από την τρέχουσα συνεδρία σας, απαιτώντας από εσάς να συνδεθείτε ξανά. Οι ενεργές συνεδρίες σε άλλες συσκευές ενδέχεται να συνεχίσουν να είναι ενεργές για μία ώρα." @@ -2179,8 +2476,11 @@ "updateWeakMasterPasswordWarning": { "message": "Ο Κύριος κωδικός πρόσβασης δεν πληροί τις απαιτήσεις πολιτικής αυτού του οργανισμού. Για να έχετε πρόσβαση στο vault, πρέπει να ενημερώσετε τον Κύριο σας κωδικό άμεσα. Η διαδικασία θα σας αποσυνδέσει από την τρέχουσα συνεδρία σας, απαιτώντας από εσάς να συνδεθείτε ξανά. Οι ενεργές συνεδρίες σε άλλες συσκευές ενδέχεται να συνεχίσουν να είναι ενεργές για μία ώρα." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Ο οργανισμός σας έχει απενεργοποιήσει την κρυπτογράφηση αξιόπιστης συσκευής. Παρακαλώ ορίστε έναν κύριο κωδικό πρόσβασης για να αποκτήσετε πρόσβαση στο θησαυροφυλάκιο σας." + }, "resetPasswordPolicyAutoEnroll": { - "message": "Αυτόματη Εγγραφή" + "message": "Αυτόματη εγγραφή" }, "resetPasswordAutoEnrollInviteWarning": { "message": "Αυτός ο οργανισμός έχει μια επιχειρηματική πολιτική που θα σας εγγράψει αυτόματα στην επαναφορά κωδικού. Η εγγραφή θα επιτρέψει στους διαχειριστές του οργανισμού να αλλάξουν τον κύριο κωδικό πρόσβασης σας." @@ -2189,19 +2489,19 @@ "message": "Επιλέξτε φάκελο..." }, "noFoldersFound": { - "message": "No folders found", + "message": "Δε βρέθηκαν φάκελοι", "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "Τα δικαιώματα του οργανισμού σας ενημερώθηκαν, απαιτώντας από εσάς να ορίσετε έναν κύριο κωδικό πρόσβασης.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "Ο οργανισμός σας απαιτεί να ορίσετε έναν κύριο κωδικό πρόσβασης.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { - "message": "Verification required", + "message": "Απαιτείται επαλήθευση", "description": "Default title for the user verification dialog." }, "hours": { @@ -2253,10 +2553,10 @@ "message": "Το χρονικό όριο του vault σας υπερβαίνει τους περιορισμούς που έχει ορίσει ο οργανισμός σας." }, "vaultExportDisabled": { - "message": "Εξαγωγή vault Απενεργοποιημένη" + "message": "Μη διαθέσιμη εξαγωγή θησαυ/κίου" }, "personalVaultExportPolicyInEffect": { - "message": "Μία ή περισσότερες οργανωτικές πολιτικές σας αποτρέπει από την εξαγωγή του προσωπικού vault." + "message": "Μία ή περισσότερες πολιτικές οργανισμού σας αποτρέπει από την εξαγωγή του προσωπικού σας θησαυ/κίου." }, "copyCustomFieldNameInvalidElement": { "message": "Δεν είναι δυνατή η αναγνώριση ενός έγκυρου στοιχείου φόρμας. Δοκιμάστε να επιθεωρήσετε τον κώδικα HTML." @@ -2277,10 +2577,10 @@ "message": "Αποχώρηση από τον οργανισμό" }, "removeMasterPassword": { - "message": "Αφαίρεση Κύριου Κωδικού Πρόσβασης" + "message": "Αφαίρεση κύριου κωδικού πρόσβασης" }, "removedMasterPassword": { - "message": "Ο κύριος κωδικός αφαιρέθηκε." + "message": "Ο κύριος κωδικός πρόσβασης αφαιρέθηκε" }, "leaveOrganizationConfirmation": { "message": "Είστε βέβαιοι ότι θέλετε να φύγετε από αυτόν τον οργανισμό;" @@ -2295,10 +2595,10 @@ "message": "Έχει λήξει το χρονικό όριο. Παρακαλώ επιστρέψτε και προσπαθήστε να συνδεθείτε ξανά." }, "exportingPersonalVaultTitle": { - "message": "Εξαγωγή Προσωπικού Vault" + "message": "Εξαγωγή ατομικού θησαυ/κίου" }, "exportingIndividualVaultDescription": { - "message": "Μόνο τα μεμονωμένα αντικείμενα θησαυ/κιου που σχετίζονται με το $EMAIL$ θα εξαχθούν. Τα αντικείμενα θησαυ/κιου οργανισμού δε θα συμπεριληφθούν. Μόνο πληροφορίες αντικειμένων θησαυ/κιου θα εξαχθούν και δε θα περιλαμβάνουν συσχετιζόμενα συνημμένα.", + "message": "Μόνο τα ατομικά αντικείμενα θησαυ/κίου που σχετίζονται με το $EMAIL$ θα εξαχθούν. Τα αντικείμενα θησαυ/κίου του οργανισμού δε θα συμπεριληφθούν. Μόνο πληροφορίες αντικειμένων θησαυ/κίου θα εξαχθούν και δε θα περιλαμβάνουν συσχετιζόμενα συνημμένα.", "placeholders": { "email": { "content": "$1", @@ -2307,10 +2607,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "Εξαγωγή θησαυ/κίου οργανισμού" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Μόνο το θησαυ/κιο του οργανισμού που σχετίζεται με το $ORGANIZATION$ θα εξαχθεί. Αντικείμενα σε ατομικά θησαυ/κια ή άλλους οργανισμούς δε θα συμπεριληφθούν.", "placeholders": { "organization": { "content": "$1", @@ -2322,23 +2622,23 @@ "message": "Σφάλμα" }, "regenerateUsername": { - "message": "Επαναδημιουργία Ονόματος Χρήστη" + "message": "Επαναδημιουργία ονόματος χρήστη" }, "generateUsername": { - "message": "Δημιουργία Όνομα Χρήστη" + "message": "Δημιουργία ονόματος χρήστη" }, "usernameType": { - "message": "Τύπος Ονόματος Χρήστη" + "message": "Τύπος ονόματος χρήστη" }, "plusAddressedEmail": { - "message": "Συν Διεύθυνση Email", + "message": "Συν διεύθυνση ηλ. ταχυδρομείου", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" }, "plusAddressedEmailDesc": { "message": "Χρησιμοποιήστε τις δυνατότητες δευτερεύουσας διεύθυνσης του παρόχου email σας." }, "catchallEmail": { - "message": "Catch-all Email" + "message": "Διεύθυνση ηλ. ταχυδρομείου κάθε σκοπού" }, "catchallEmailDesc": { "message": "Χρησιμοποιήστε τα διαμορφωμένα εισερχόμενα catch-all του domain σας." @@ -2347,28 +2647,28 @@ "message": "Τυχαίο" }, "randomWord": { - "message": "Τυχαία Λέξη" + "message": "Τυχαία λέξη" }, "websiteName": { - "message": "Όνομα Ιστοσελίδας" + "message": "Όνομα ιστοσελίδας" }, "whatWouldYouLikeToGenerate": { "message": "Τι θα θέλατε να δημιουργήσετε?" }, "passwordType": { - "message": "Τύπος Κωδικού" + "message": "Τύπος κωδικού πρόσβασης" }, "service": { "message": "Υπηρεσία" }, "forwardedEmail": { - "message": "Προωθημένο Email Alias" + "message": "Προωθημένο ψευδώνυμο διεύθυνσης ηλ. ταχυδρομείου" }, "forwardedEmailDesc": { "message": "Δημιουργήστε ένα alias email με μια εξωτερική υπηρεσία προώθησης." }, "forwarderError": { - "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "message": "$SERVICENAME$ σφάλμα: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", "placeholders": { "servicename": { @@ -2382,11 +2682,11 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "Δημιουργήθηκε από το Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "Ιστοσελίδα: $WEBSITE$. Δημιουργήθηκε από το Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -2396,7 +2696,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "Μη έγκυρο $SERVICENAME$ διακριτικό API", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -2406,7 +2706,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "Μη έγκυρο $SERVICENAME$ API διακριτικό: $ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -2420,7 +2720,7 @@ } }, "forwarderNoAccountId": { - "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "message": "Αδύνατη η απόκτηση του $SERVICENAME$ καμουφλαρισμένου ID διεύθυνσης ηλ. ταχυδρομείου.", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -2430,7 +2730,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "Μη έγκυρος $SERVICENAME$ τομέας.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -2440,7 +2740,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "Μη έγκυρο $SERVICENAME$ url.", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -2450,7 +2750,7 @@ } }, "forwarderUnknownError": { - "message": "Unknown $SERVICENAME$ error occurred.", + "message": "Παρουσιάστηκε άγνωστο $SERVICENAME$ σφάλμα.", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -2460,7 +2760,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Unknown forwarder: '$SERVICENAME$'.", + "message": "Άγνωστος διαβιβαστής: '$SERVICENAME$'.", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -2480,13 +2780,13 @@ "message": "Kλειδί API" }, "ssoKeyConnectorError": { - "message": "Σφάλμα Key Connector: βεβαιωθείτε ότι το Key Connector είναι διαθέσιμο και λειτουργεί σωστά." + "message": "Σφάλμα Key connector: βεβαιωθείτε ότι το Key connector είναι διαθέσιμο και λειτουργεί σωστά." }, "premiumSubcriptionRequired": { "message": "Απαιτείται συνδρομή Premium" }, "organizationIsDisabled": { - "message": "Ο οργανισμός είναι απενεργοποιημένος." + "message": "Ο οργανισμός αναστάλθηκε." }, "disabledOrganizationFilterError": { "message": "Δεν είναι δυνατή η πρόσβαση σε αντικείμενα σε απενεργοποιημένους οργανισμούς. Επικοινωνήστε με τον ιδιοκτήτη του Οργανισμού για βοήθεια." @@ -2546,19 +2846,19 @@ "message": "Δεν είστε εσείς;" }, "newAroundHere": { - "message": "Νέος/α στα μέρη μας;" + "message": "Είστε νέος/α εδώ;" }, "rememberEmail": { - "message": "Απομνημόνευση email" + "message": "Απομνημόνευση διεύθυνσης ηλ. ταχυδρομείου" }, "loginWithDevice": { "message": "Σύνδεση με τη χρήση συσκευής" }, "loginWithDeviceEnabledInfo": { - "message": "Η σύνδεση με τη χρήση συσκευής πρέπει να έχει ρυθμιστεί στις ρυθμίσεις της εφαρμογής Bitwarden. Χρειάζεστε κάποια άλλη επιλογή;" + "message": "Η σύνδεση με τη χρήση συσκευής πρέπει να οριστεί στις ρυθμίσεις της εφαρμογής Bitwarden. Χρειάζεστε κάποια άλλη επιλογή;" }, "fingerprintPhraseHeader": { - "message": "Φράση δακτυλικών αποτυπωμάτων" + "message": "Φράση δακτυλικού αποτυπώματος" }, "fingerprintMatchInfo": { "message": "Βεβαιωθείτε ότι το vault σας είναι ξεκλειδωμένο και η Φράση δακτυλικών αποτυπωμάτων ταιριάζει στην άλλη συσκευή." @@ -2582,13 +2882,13 @@ "message": "Ο κωδικός έχει βρεθεί σε παραβίαση δεδομένων. Χρησιμοποιήστε έναν μοναδικό κωδικό για την προστασία του λογαριασμού σας. Είστε σίγουροι ότι θέλετε να χρησιμοποιήσετε έναν εκτεθειμένο κωδικό πρόσβασης;" }, "weakAndExposedMasterPassword": { - "message": "Αδύναμος και εκτεθειμένος Κύριος Κωδικός Πρόσβασης" + "message": "Αδύναμος και Εκτεθειμένος Κύριος Κωδικός Πρόσβασης" }, "weakAndBreachedMasterPasswordDesc": { "message": "Αδύναμος κωδικός που έχει εντοπιστεί σε παραβίαση δεδομένων. Χρησιμοποιήστε έναν ισχυρό και μοναδικό κωδικό για την προστασία του λογαριασμού σας. Είστε σίγουροι ότι θέλετε να χρησιμοποιήσετε αυτόν τον κωδικό;" }, "checkForBreaches": { - "message": "Ελέγξτε γνωστές παραβιάσεις δεδομένων για αυτόν τον κωδικό πρόσβασης" + "message": "Ελέγξτε γνωστές διαρροές δεδομένων για αυτόν τον κωδικό πρόσβασης" }, "important": { "message": "Σημαντικό:" @@ -2612,7 +2912,7 @@ "message": "Πώς να συμπληρώσετε αυτόματα" }, "autofillSelectInfoWithCommand": { - "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", + "message": "Επιλέξτε ένα αντικείμενο από αυτήν την οθόνη, χρησιμοποιήστε τη συντόμευση $COMMAND$, ή εξερευνήστε άλλες επιλογές στις ρυθμίσεις.", "placeholders": { "command": { "content": "$1", @@ -2621,7 +2921,7 @@ } }, "autofillSelectInfoWithoutCommand": { - "message": "Select an item from this screen, or explore other options in settings." + "message": "Επιλέξτε ένα αντικείμενο από αυτήν την οθόνη ή εξερευνήστε άλλες επιλογές στις ρυθμίσεις." }, "gotIt": { "message": "Το κατάλαβα" @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Ρυθμίσεις αυτόματης συμπλήρωσης" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Συντόμευση αυτόματης συμπλήρωσης" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Αλλαγή συντόμευσης" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Διαχείριση συντομεύσεων" + }, "autofillShortcut": { "message": "Συντόμευση πληκτρολογίου αυτόματης συμπλήρωσης" }, - "autofillShortcutNotSet": { - "message": "Η συντόμευση αυτόματης συμπλήρωσης δεν έχει οριστεί. Αλλάξτε τη στις ρυθμίσεις του περιηγητή." + "autofillLoginShortcutNotSet": { + "message": "Η συντόμευση αυτόματης συμπλήρωσης σύνδεσης δεν έχει οριστεί. Αλλάξτε το αυτό στις ρυθμίσεις του περιηγητή." }, - "autofillShortcutText": { - "message": "Η συντόμευση αυτόματης συμπλήρωσης είναι: $COMMAND$. Αλλάξτε τη στις ρυθμίσεις του προγράμματος περιήγησης.", + "autofillLoginShortcutText": { + "message": "Η συντόμευση αυτόματης συμπλήρωσης σύνδεσης είναι $COMMAND$. Διαχειριστείτε όλες τις συντομεύσεις στις ρυθμίσεις του περιηγητή.", "placeholders": { "command": { "content": "$1", @@ -2654,7 +2963,7 @@ } }, "loggingInOn": { - "message": "Σύνδεση ως" + "message": "Σύνδεση στο" }, "opensInANewWindow": { "message": "Ανοίγει σε νέο παράθυρο" @@ -2675,31 +2984,31 @@ "message": "Αίτηση έγκρισης διαχειριστή" }, "approveWithMasterPassword": { - "message": "Έγκριση με τον κύριο κωδικό" + "message": "Έγκριση με κύριο κωδικό πρόσβασης" }, "ssoIdentifierRequired": { "message": "Απαιτείται αναγνωριστικό οργανισμού SSO." }, "creatingAccountOn": { - "message": "Creating account on" + "message": "Δημιουργία λογαριασμού στο" }, "checkYourEmail": { - "message": "Check your email" + "message": "Ελέγξτε το ηλ. ταχυδρομείο σας" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "Ακολουθήστε το σύνδεσμο στο μήνυμα ηλ. ταχυδρομείου που στάλθηκε στο" }, "andContinueCreatingYourAccount": { - "message": "and continue creating your account." + "message": "και συνεχίστε στη δημιουργία του λογαριασμού σας." }, "noEmail": { - "message": "No email?" + "message": "Κανένα μήνυμα ηλ. ταχυδρομείου;" }, "goBack": { - "message": "Go back" + "message": "Επιστροφή" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "για να επεξεργαστείτε τη διεύθυνση ηλ. ταχυδρομείου σας." }, "eu": { "message": "ΕΕ", @@ -2733,11 +3042,19 @@ "message": "Η σύνδεση εγκρίθηκε" }, "userEmailMissing": { - "message": "Το email του χρήστη απουσιάζει" + "message": "Η διεύθυνση ηλ. ταχυδρομείου του χρήστη λείπει" }, "deviceTrusted": { "message": "Αξιόπιστη συσκευή" }, + "sendsNoItemsTitle": { + "message": "Κανένα ενεργό Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Χρήση Send για ασφαλή κοινοποίηση κρυπτογραφημένων πληροφοριών με οποιονδήποτε.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Απαιτείται εισαγωγή." }, @@ -2775,7 +3092,7 @@ } }, "inputMinValue": { - "message": "Η τιμή καταχώρησης πρέπει να είναι τουλάχιστον $MIN$", + "message": "Η τιμή καταχώρησης πρέπει να είναι τουλάχιστον $MIN$.", "placeholders": { "min": { "content": "$1", @@ -2793,17 +3110,17 @@ } }, "multipleInputEmails": { - "message": "1 ή περισσότερα email δεν είναι έγκυρα" + "message": "1 ή περισσότερες διευθύνσεις ηλ. ταχυδρομείου δεν είναι έγκυρες" }, "inputTrimValidator": { "message": "Η καταχώρηση δεν πρέπει να περιέχει μόνο κενά.", "description": "Notification to inform the user that a form's input can't contain only whitespace." }, "inputEmail": { - "message": "Η καταχώρηση δεν είναι διεύθυνση email." + "message": "Η καταχώρηση δεν είναι διεύθυνση ηλ. ταχυδρομείου." }, "fieldsNeedAttention": { - "message": "$COUNT$ Το/α παραπάνω πεδίo/α χρειάζονται την προσοχή σας.", + "message": "Το/α $COUNT$ παραπάνω πεδίo/α χρειάζονται την προσοχή σας.", "placeholders": { "count": { "content": "$1", @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 πεδίο χρειάζεται την προσοχή σας." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ πεδία χρειάζονται την προσοχή σας.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Επιλογή --" }, @@ -2843,114 +3172,146 @@ "description": "Toggling an expand/collapse state." }, "filelessImport": { - "message": "Import your data to Bitwarden?", + "message": "Εισαγωγή των δεδομένων σας στο Bitwarden;", "description": "Default notification title for triggering a fileless import." }, "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", + "message": "Προστατέψτε τα δεδομένα LastPass και εισαγάγετε στο Bitwarden;", "description": "LastPass specific notification title for triggering a fileless import." }, "lpCancelFilelessImport": { - "message": "Save as unencrypted file", + "message": "Αποθήκευση ως μη κρυπτογραφημένο αρχείο", "description": "LastPass specific notification button text for cancelling a fileless import." }, "startFilelessImport": { - "message": "Import to Bitwarden", + "message": "Εισαγωγή στο Bitwarden", "description": "Notification button text for starting a fileless import." }, "importing": { - "message": "Importing...", + "message": "Εισαγωγή...", "description": "Notification message for when an import is in progress." }, "dataSuccessfullyImported": { - "message": "Data successfully imported!", + "message": "Επιτυχής εισαγωγή δεδομένων!", "description": "Notification message for when an import has completed successfully." }, "dataImportFailed": { - "message": "Error importing. Check console for details.", + "message": "Σφάλμα εισαγωγής. Ελέγξτε την κονσόλα για λεπτομέρειες.", "description": "Notification message for when an import has failed." }, "importNetworkError": { - "message": "Network error encountered during import.", + "message": "Εμφανίστηκε σφάλμα δικτύου κατά την εισαγωγή.", "description": "Notification message for when an import has failed due to a network error." }, "aliasDomain": { - "message": "Συνώνυμο domain" + "message": "Ψευδώνυμο τομέα" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Τα αντικείμενα με υπόδειξη κύριου κωδικού πρόσβασης δεν μπορούν να συμπληρωθούν αυτόματα κατά τη φόρτωση της σελίδας. Η αυτόματη συμπλήρωση έχει απενεργοποιηθεί κατά τη φόρτωση της σελίδας.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Η αυτόματη συμπλήρωση κατά τη φόρτωση της σελίδας ορίστηκε να χρησιμοποιεί τις προεπιλεγμένες ρυθμίσεις.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { - "message": "Turn off master password re-prompt to edit this field", + "message": "Απενεργοποιήστε την υπόδειξη κύριου κωδικού πρόσβασης για να επεξεργαστείτε αυτό το πεδίο", "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "Εναλλαγή πλευρικής πλοήγησης" }, "skipToContent": { - "message": "Skip to content" + "message": "Μετάβαση στο περιεχόμενο" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Πλήκτρο μενού αυτόματης συμπλήρωσης Bitwarden", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Εναλλαγή ορατότητας μενού αυτόματης συμπλήρωσης Bitwarden", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Μενού αυτόματης συμπλήρωσης Bitwarden", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { - "message": "Unlock your account to view matching logins", + "message": "Ξεκλειδώστε τον λογαριασμό σας για να δείτε συνδέσεις που ταιριάζουν", + "description": "Text to display in overlay when the account is locked." + }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Ξεκλειδώστε τον λογαριασμό σας για να δείτε προτάσεις αυτόματης συμπλήρωσης", "description": "Text to display in overlay when the account is locked." }, "unlockAccount": { - "message": "Unlock account", + "message": "Ξεκλείδωμα λογαριασμού", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Ξεκλείδωμα του λογαριασμού σας, ανοίγει σε νέο παράθυρο", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { - "message": "Fill credentials for", + "message": "Συμπλήρωση στοιχείων για", "description": "Screen reader text for when overlay item is in focused" }, "partialUsername": { - "message": "Partial username", + "message": "Μερικό όνομα χρήστη", "description": "Screen reader text for when a login item is focused where a partial username is displayed. SR will announce this phrase before reading the text of the partial username" }, "noItemsToShow": { - "message": "No items to show", + "message": "Δεν υπάρχουν αντικείμενα για προβολή", "description": "Text to show in overlay if there are no matching items" }, "newItem": { - "message": "New item", + "message": "Νέο αντικείμενο", "description": "Button text to display in overlay when there are no matching items" }, "addNewVaultItem": { - "message": "Add new vault item", + "message": "Προσθήκη νέου αντικειμένου θησαυ/κίου", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "Νέα σύνδεση", + "description": "Button text to display within inline menu when there are no matching items on a login field" + }, + "addNewLoginItemAria": { + "message": "Προσθήκη νέου αντικειμένου σύνδεσης θησαυ/κίου, ανοίγει σε νέο παράθυρο", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "Νέα κάρτα", + "description": "Button text to display within inline menu when there are no matching items on a credit card field" + }, + "addNewCardItemAria": { + "message": "Προσθήκη νέου αντικειμένου κάρτας θησαυ/κίου, ανοίγει σε νέο παράθυρο", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "Νέα ταυτότητα", + "description": "Button text to display within inline menu when there are no matching items on an identity field" + }, + "addNewIdentityItemAria": { + "message": "Προσθήκη νέου αντικειμένου ταυτότητας θησαυ/κίου, ανοίγει σε νέο παράθυρο", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Είναι διαθέσιμο το Bitwarden μενού αυτόματης συμπλήρωσης. Πατήστε το πλήκτρο κάτω βέλος για να επιλέξετε.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { - "message": "Turn on" + "message": "Ενεργοποίηση" }, "ignore": { - "message": "Ignore" + "message": "Παράβλεψη" }, "importData": { "message": "Εισαγωγή δεδομένων", "description": "Used for the header of the import dialog, the import button and within the file-password-prompt" }, "importError": { - "message": "Σφάλμα κατά την εισαγωγή" + "message": "Σφάλμα εισαγωγής" }, "importErrorDesc": { "message": "Παρουσιάστηκε πρόβλημα με τα δεδομένα που επιχειρήσατε να εισαγάγετε. Παρακαλώ επιλύστε τα σφάλματα που αναφέρονται παρακάτω στο αρχείο προέλευσης και προσπαθήστε ξανά." @@ -2965,7 +3326,7 @@ "message": "Τα δεδομένα εισήχθησαν επιτυχώς" }, "importSuccessNumberOfItems": { - "message": "Ένα σύνολο $AMOUNT$ στοιχείων εισήχθησαν.", + "message": "Ένα σύνολο $AMOUNT$ αντικειμένων εισήχθησαν.", "placeholders": { "amount": { "content": "$1", @@ -2974,40 +3335,40 @@ } }, "tryAgain": { - "message": "Try again" + "message": "Προσπαθήστε ξανά" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "Απαιτείται επαλήθευση για αυτήν την ενέργεια. Ορίστε ένα PIN για να συνεχίσετε." }, "setPin": { - "message": "Set PIN" + "message": "Ορισμός PIN" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Επαλήθευση με βιομετρικά" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Σε αναμονή επιβεβαίωσης" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "Αδύνατη η ολοκλήρωση των βιομετρικών." }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "Χρειάζεστε μια διαφορετική μέθοδο;" }, "useMasterPassword": { - "message": "Use master password" + "message": "Χρήση κύριου κωδικού πρόσβασης" }, "usePin": { - "message": "Use PIN" + "message": "Χρήση PIN" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Χρήση βιομετρικών" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "Εισάγετε τον κωδικό επαλήθευσης που έχει σταλεί στο ηλ. ταχυδρομείο σας." }, "resendCode": { - "message": "Resend code" + "message": "Επαναποστολή κωδικού" }, "total": { "message": "Σύνολο" @@ -3021,20 +3382,23 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Σφάλμα κατά τη σύνδεση με την υπηρεσία Duo. Χρησιμοποιήστε μια διαφορετική μέθοδο σύνδεσης δύο βημάτων ή επικοινωνήστε με την Duo για βοήθεια." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "Εκκινήστε το Duo και ακολουθήστε τα βήματα για να ολοκληρώσετε τη σύνδεση." }, "duoRequiredForAccount": { - "message": "Duo two-step login is required for your account." + "message": "Απαιτείται σύνδεση δύο βημάτων Duo για το λογαριασμό σας." }, "popoutTheExtensionToCompleteLogin": { - "message": "Popout the extension to complete login." + "message": "Ανοίξτε την επέκταση σε νέο παράθυρο για να ολοκληρώσετε τη σύνδεση." }, "popoutExtension": { - "message": "Popout extension" + "message": "Άνοιγμα επέκτασης σε νέο παράθυρο" }, "launchDuo": { - "message": "Launch Duo" + "message": "Εκκίνηση Duo" }, "importFormatError": { "message": "Τα δεδομένα δεν έχουν διαμορφωθεί σωστά. Ελέγξτε το αρχείο εισαγωγής και δοκιμάστε ξανά." @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Μη έγκυρος κωδικός πρόσβασης, παρακαλώ χρησιμοποιήστε τον κωδικό πρόσβασης που εισαγάγατε όταν δημιουργήσατε το αρχείο εξαγωγής." }, - "importDestination": { - "message": "Προορισμός εισαγωγής" + "destination": { + "message": "Προορισμός" }, "learnAboutImportOptions": { "message": "Μάθετε για τις επιλογές εισαγωγής σας" @@ -3061,7 +3425,7 @@ "message": "Επιλέξτε μια συλλογή" }, "importTargetHint": { - "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", + "message": "Επιλέξτε αυτή την επιλογή εάν θέλετε τα περιεχόμενα του εισαγόμενου αρχείου να μετακινηθούν στο $DESTINATION$", "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", "placeholders": { "destination": { @@ -3074,7 +3438,7 @@ "message": "Το αρχείο περιέχει μη συσχετισμένα στοιχεία." }, "selectFormat": { - "message": "Επιλέξτε τη μορφή του αρχείου εισαγωγής" + "message": "Επιλέξτε τον τύπο του αρχείου εισαγωγής" }, "selectImportFile": { "message": "Επιλέξτε το αρχείο εισαγωγής" @@ -3099,7 +3463,7 @@ } }, "confirmVaultImport": { - "message": "Επιβεβαίωση εισαγωγής θησαυροφυλακίου" + "message": "Επιβεβαίωση εισαγωγής θησαυ/κίου" }, "confirmVaultImportDesc": { "message": "Αυτό το αρχείο προστατεύεται με κωδικό πρόσβασης. Παρακαλώ εισαγάγετε τον κωδικό πρόσβασης για την εισαγωγή δεδομένων." @@ -3108,58 +3472,64 @@ "message": "Επιβεβαίωση κωδικού πρόσβασης αρχείου" }, "exportSuccess": { - "message": "Vault data exported" + "message": "Εξήχθησαν τα δεδομένα θησαυ/κίου" }, "typePasskey": { - "message": "Passkey" + "message": "Κλειδί πρόσβασης" }, "passkeyNotCopied": { - "message": "Passkey will not be copied" + "message": "Το κλειδί πρόσβασης δε θα αντιγραφεί" }, "passkeyNotCopiedAlert": { - "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" + "message": "Το κλειδί πρόσβασης δε θα αντιγραφεί στο κλωνοποιημένο αντικείμενο. Θέλετε να συνεχίσετε την κλωνοποίηση αυτού του αντικειμένου;" }, "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Απαιτείται επαλήθευση από τον ιστότοπο εκκίνησης. Αυτή η λειτουργία δεν έχει ακόμα υλοποιηθεί για λογαριασμούς χωρίς τον κύριο κωδικό πρόσβασης." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { - "message": "A passkey already exists for this application." + "message": "Υπάρχει ήδη ένα κλειδί πρόσβασης για αυτήν την εφαρμογή." }, "noPasskeysFoundForThisApplication": { - "message": "No passkeys found for this application." + "message": "Δε βρέθηκαν κλειδιά πρόσβασης για αυτήν την εφαρμογή." }, "noMatchingPasskeyLogin": { "message": "Δεν έχετε στοιχεία σύνδεσης που να συνδυάζονται με αυτόν τον ιστότοπο." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Επιβεβαίωση" }, "savePasskey": { - "message": "Save passkey" + "message": "Αποθήκευση κλειδιού πρόσβασης" }, "savePasskeyNewLogin": { - "message": "Save passkey as new login" + "message": "Αποθήκευση κλειδιού πρόσβασης ως νέα σύνδεση" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { - "message": "Passkey Item" + "message": "Αντικείμενο κλειδιού πρόσβασης" }, "overwritePasskey": { - "message": "Overwrite passkey?" + "message": "Αντικατάσταση κλειδιού πρόσβασης;" }, "overwritePasskeyAlert": { - "message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?" + "message": "Αυτό το αντικείμενο περιέχει ήδη ένα κλειδί πρόσβασης. Είστε σίγουροι ότι θέλετε να αντικαταστήσετε το τρέχον κλειδί πρόσβασης;" }, "featureNotSupported": { "message": "Η λειτουργία δεν υποστηρίζεται ακόμη" }, "yourPasskeyIsLocked": { - "message": "Authentication required to use passkey. Verify your identity to continue." + "message": "Απαιτείται αυθεντικοποίηση για χρήση κλειδιού πρόσβασης. Επαληθεύστε την ταυτότητά σας για να συνεχίσετε." }, "multifactorAuthenticationCancelled": { "message": "Ο πολυμερής έλεγχος ταυτότητας ακυρώθηκε" @@ -3168,16 +3538,16 @@ "message": "Δεν βρέθηκαν δεδομένα LastPass" }, "incorrectUsernameOrPassword": { - "message": "Λάθος όνομα χρήστη ή κωδικού πρόσβασης" + "message": "Λάθος όνομα χρήστη ή κωδικός πρόσβασης" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "Εσφαλμένος κωδικός πρόσβασης" }, "incorrectCode": { - "message": "Incorrect code" + "message": "Εσφαλμένος κωδικός" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "Εσφαλμένο PIN" }, "multifactorAuthenticationFailed": { "message": "Ο πολυμερής έλεγχος ταυτότητας απέτυχε" @@ -3186,7 +3556,7 @@ "message": "Συμπερίληψη κοινόχρηστων φακέλων" }, "lastPassEmail": { - "message": "Διεύθυνση Αλληλογραφίας Lastpass" + "message": "Διεύθυνση ηλ. ταχυδρομείου LastPass" }, "importingYourAccount": { "message": "Εισαγωγή του λογαριασμού σας..." @@ -3201,7 +3571,7 @@ "message": "Εγκρίνετε το αίτημα σύνδεσης στην εφαρμογή ελέγχου ταυτότητας ή εισαγάγετε έναν κωδικό πρόσβασης μιας χρήσης." }, "passcode": { - "message": "Passcode" + "message": "Κωδικός" }, "lastPassMasterPassword": { "message": "Κύριος κωδικός πρόσβασης LastPass" @@ -3216,7 +3586,7 @@ "message": "Παρακαλούμε συνεχίστε τη σύνδεση χρησιμοποιώντας τα στοιχεία της εταιρείας σας." }, "seeDetailedInstructions": { - "message": "Δείτε λεπτομερείς οδηγίες στην ιστοσελίδα βοήθειας μας στο", + "message": "Δείτε λεπτομερείς οδηγίες στη βοηθητική ιστοσελίδα μας στο", "description": "This is followed a by a hyperlink to the help website." }, "importDirectlyFromLastPass": { @@ -3226,145 +3596,163 @@ "message": "Εισαγωγή από CSV" }, "lastPassTryAgainCheckEmail": { - "message": "Δοκιμάστε ξανά ή ψάξτε για ένα email από το LastPass για να επιβεβαιώσετε ότι είστε εσείς." + "message": "Δοκιμάστε ξανά ή ψάξτε για ένα μήνυμα ηλ. ταχυδρομείου από το LastPass για να επιβεβαιώσετε ότι είστε εσείς." }, "collection": { "message": "Συλλογή" }, "lastPassYubikeyDesc": { - "message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button." + "message": "Εισάγετε το YubiKey που σχετίζεται με τον λογαριασμό LastPass στη θύρα USB του υπολογιστή σας, και στη συνέχεια αγγίξτε το κουμπί του." }, "switchAccount": { - "message": "Switch account" + "message": "Αλλαγή λογαριασμού" }, "switchAccounts": { - "message": "Switch accounts" + "message": "Αλλαγή λογαριασμών" }, "switchToAccount": { - "message": "Switch to account" + "message": "Αλλαγή σε λογαριασμό" }, "activeAccount": { - "message": "Active account" + "message": "Ενεργός λογαριασμός" }, "availableAccounts": { - "message": "Available accounts" + "message": "Διαθέσιμοι λογαριασμοί" }, "accountLimitReached": { - "message": "Account limit reached. Log out of an account to add another." + "message": "Συμπληρώθηκε το όριο λογαριασμού. Αποσυνδεθείτε από έναν λογαριασμό για να προσθέσετε έναν άλλο." }, "active": { - "message": "active" + "message": "ενεργό" }, "locked": { - "message": "locked" + "message": "κλειδωμένο" }, "unlocked": { - "message": "unlocked" + "message": "ξεκλείδωτο" }, "server": { - "message": "server" + "message": "διακομιστής" }, "hostedAt": { - "message": "hosted at" + "message": "φιλοξενούμενο σε" }, "useDeviceOrHardwareKey": { - "message": "Use your device or hardware key" + "message": "Χρήση της συσκευής ή του φυσικού κλειδιού σας" }, "justOnce": { - "message": "Just once" + "message": "Μόνο μία φορά" }, "alwaysForThisSite": { - "message": "Always for this site" + "message": "Πάντα για αυτήν την ιστοσελίδα" }, "domainAddedToExcludedDomains": { - "message": "$DOMAIN$ added to excluded domains.", + "message": "Το $DOMAIN$ προστέθηκε στους εξαιρούμενους τομείς.", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, "commonImportFormats": { - "message": "Common formats", + "message": "Κοινοί τύποι", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Συνεχίστε στις ρυθμίσεις περιηγητή;", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Συνέχεια στο Κέντρο Βοήθειας;", + "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" + }, + "confirmContinueToHelpCenterPasswordManagementContent": { + "message": "Αλλάξτε τις ρυθμίσεις αυτόματης συμπλήρωσης και διαχείρισης κωδικών πρόσβασης του περιηγητή σας.", + "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": "Μπορείτε να δείτε και να ορίσετε συντομεύσεις επεκτάσεων στις ρυθμίσεις του περιηγητή σας.", + "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": "Αλλάξτε τις ρυθμίσεις αυτόματης συμπλήρωσης και διαχείρισης κωδικών πρόσβασης του περιηγητή σας.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" + }, + "confirmContinueToBrowserKeyboardShortcutSettingsContent": { + "message": "Μπορείτε να δείτε και να ορίσετε συντομεύσεις επεκτάσεων στις ρυθμίσεις του περιηγητή σας.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { - "message": "Make Bitwarden your default password manager?", + "message": "Να γίνει το Bitwarden ο προεπιλεγμένος διαχειριστής κωδικών πρόσβασης σας;", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Η αγνόηση αυτής της επιλογής μπορεί να προκαλέσει συγκρούσεις μεταξύ του μενού αυτόματης συμπλήρωσης Bitwarden και του περιηγητή σας.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { - "message": "Make Bitwarden your default password manager", + "message": "Να γίνει το Bitwarden ο προεπιλεγμένος διαχειριστής κωδικών πρόσβασης σας", "description": "Label for the setting that allows overriding the default browser autofill settings" }, "privacyPermissionAdditionNotGrantedTitle": { - "message": "Unable to set Bitwarden as the default password manager", + "message": "Αδυναμία ορισμού του Bitwarden ως προεπιλεγμένου διαχειριστή κωδικών πρόσβασης", "description": "Title for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "privacyPermissionAdditionNotGrantedDescription": { - "message": "You must grant browser privacy permissions to Bitwarden to set it as the default password manager.", + "message": "Πρέπει να παραχωρήσετε δικαιώματα απορρήτου περιηγητή στο Bitwarden για να το ορίσετε ως προεπιλεγμένο διαχειριστή κωδικών πρόσβασης.", "description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "makeDefault": { - "message": "Make default", + "message": "Ορισμός ως προεπιλογή", "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { - "message": "Credentials saved successfully!", + "message": "Τα στοιχεία αποθηκεύτηκαν επιτυχώς!", + "description": "Notification message for when saving credentials has succeeded." + }, + "passwordSaved": { + "message": "Ο κωδικός πρόσβασης αποθηκεύτηκε!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { - "message": "Credentials updated successfully!", + "message": "Τα στοιχεία ενημερώθηκαν επιτυχώς!", + "description": "Notification message for when updating credentials has succeeded." + }, + "passwordUpdated": { + "message": "Ο κωδικός πρόσβασης ενημερώθηκε!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { - "message": "Error saving credentials. Check console for details.", + "message": "Σφάλμα αποθήκευσης στοιχείων. Ελέγξτε την κονσόλα για λεπτομέρειες.", "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Επιτυχία" }, "removePasskey": { - "message": "Remove passkey" + "message": "Αφαίρεση κλειδιού πρόσβασης" }, "passkeyRemoved": { - "message": "Passkey removed" - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + "message": "Το κλειδί πρόσβασης αφαιρέθηκε" }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Προτάσεις αυτόματης συμπλήρωσης" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Αποθηκεύστε ένα αντικείμενο σύνδεσης για την αυτόματη συμπλήρωση αυτού του ιστοτόπου" }, "yourVaultIsEmpty": { - "message": "Your vault is empty" + "message": "Το θησαυ/κιό σας είναι άδειο" }, "noItemsMatchSearch": { - "message": "No items match your search" + "message": "Κανένα αντικείμενο δεν ταιριάζει με την αναζήτησή σας" }, "clearFiltersOrTryAnother": { - "message": "Clear filters or try another search term" + "message": "Καθαρισμός φίλτρων ή δοκιμή άλλου όρου αναζήτησης" }, "copyInfoTitle": { - "message": "Copy info - $ITEMNAME$", + "message": "Αντιγραφή πληροφοριών - $ITEMNAME$", "description": "Title for a button that opens a menu with options to copy information from an item.", "placeholders": { "itemname": { @@ -3374,7 +3762,7 @@ } }, "copyNoteTitle": { - "message": "Copy Note - $ITEMNAME$", + "message": "Αντιγραφή Σημείωσης - $ITEMNAME$", "description": "Title for a button copies a note to the clipboard.", "placeholders": { "itemname": { @@ -3384,7 +3772,7 @@ } }, "moreOptionsLabel": { - "message": "More options, $ITEMNAME$", + "message": "Περισσότερες επιλογές, $ITEMNAME$", "description": "Aria label for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -3394,7 +3782,7 @@ } }, "moreOptionsTitle": { - "message": "More options - $ITEMNAME$", + "message": "Περισσότερες επιλογές - $ITEMNAME$", "description": "Title for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -3404,7 +3792,7 @@ } }, "viewItemTitle": { - "message": "View item - $ITEMNAME$", + "message": "Προβολή αντικειμένου - $ITEMNAME$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Αυτόματη συμπλήρωση - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3424,40 +3812,40 @@ } }, "noValuesToCopy": { - "message": "No values to copy" + "message": "Δεν υπάρχουν τιμές για αντιγραφή" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Ανάθεση σε συλλογές" }, "copyEmail": { - "message": "Copy email" + "message": "Αντιγραφή διεύθυνσης ηλ. ταχυδρομείου" }, "copyPhone": { - "message": "Copy phone" + "message": "Αντιγραφή τηλεφώνου" }, "copyAddress": { - "message": "Copy address" + "message": "Αντιγραφή διεύθυνσης" }, "adminConsole": { - "message": "Admin Console" + "message": "Κονσόλα Διαχειριστή" }, "accountSecurity": { - "message": "Account security" + "message": "Ασφάλεια λογαριασμού" }, "notifications": { - "message": "Notifications" + "message": "Ειδοποιήσεις" }, "appearance": { - "message": "Appearance" + "message": "Εμφάνιση" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Σφάλμα κατά την ανάθεση συλλογής προορισμού." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Σφάλμα κατά την ανάθεση φακέλου προορισμού." }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "Προβολή αντικειμένων στο $NAME$", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -3467,7 +3855,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "Επιστροφή στο $NAME$", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -3477,10 +3865,10 @@ } }, "new": { - "message": "New" + "message": "Νέο" }, "removeItem": { - "message": "Remove $NAME$", + "message": "Αφαίρεση $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -3490,16 +3878,16 @@ } }, "itemsWithNoFolder": { - "message": "Items with no folder" + "message": "Αντικείμενα χωρίς φάκελο" }, "itemDetails": { - "message": "Item details" + "message": "Λεπτομέρειες αντικειμένου" }, "itemName": { - "message": "Item name" + "message": "Όνομα αντικειμένου" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Δεν μπορείτε να αφαιρέσετε συλλογές που έχουν μόνο δικαιώματα Προβολής: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3508,29 +3896,47 @@ } }, "organizationIsDeactivated": { - "message": "Organization is deactivated" + "message": "Ο οργανισμός απενεργοποιήθηκε" }, "owner": { - "message": "Owner" + "message": "Ιδιοκτήτης" }, "selfOwnershipLabel": { - "message": "You", + "message": "Εσείς", "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": "Δεν είναι δυνατή η πρόσβαση αντικειμένων σε απενεργοποιημένους οργανισμούς. Επικοινωνήστε με τον ιδιοκτήτη του οργανισμού σας για βοήθεια." + }, + "additionalInformation": { + "message": "Επιπρόσθετες πληροφορίες" + }, + "itemHistory": { + "message": "Ιστορικό αντικειμένων" + }, + "lastEdited": { + "message": "Τελευταία τροποποίηση" + }, + "ownerYou": { + "message": "Ιδιοκτήτης: Εσείς" + }, + "linked": { + "message": "Συνδεδεμένο" + }, + "copySuccessful": { + "message": "Επιτυχής Αντιγραφή" }, "upload": { - "message": "Upload" + "message": "Μεταφόρτωση" }, "addAttachment": { - "message": "Add attachment" + "message": "Προσθήκη συνημμένου" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "Το μέγιστο μέγεθος αρχείου είναι 500 MB" }, "deleteAttachmentName": { - "message": "Delete attachment $NAME$", + "message": "Διαγραφή συνημμένου $NAME$", "placeholders": { "name": { "content": "$1", @@ -3539,7 +3945,7 @@ } }, "downloadAttachmentName": { - "message": "Download $NAME$", + "message": "Λήψη $NAME$", "placeholders": { "name": { "content": "$1", @@ -3548,15 +3954,389 @@ } }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Are you sure you want to permanently delete this attachment?" + "message": "Είστε σίγουροι ότι θέλετε να διαγράψετε οριστικά αυτό το συνημμένο;" }, "premium": { "message": "Premium" }, "freeOrgsCannotUseAttachments": { - "message": "Free organizations cannot use attachments" + "message": "Οι δωρεάν οργανισμοί δεν μπορούν να χρησιμοποιήσουν συνημμένα" }, "filters": { - "message": "Filters" + "message": "Φίλτρα" + }, + "personalDetails": { + "message": "Προσωπικές πληροφορίες" + }, + "identification": { + "message": "Ταυτοποίηση" + }, + "contactInfo": { + "message": "Στοιχεία επικοινωνίας" + }, + "downloadAttachment": { + "message": "Λήψη - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "ο αριθμός κάρτας τελειώνει σε", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Στοιχεία σύνδεσης" + }, + "authenticatorKey": { + "message": "Κλειδί αυθεντικοποίησης" + }, + "autofillOptions": { + "message": "Επιλογές αυτόματης συμπλήρωσης" + }, + "websiteUri": { + "message": "Ιστοσελίδα (URI)" + }, + "websiteUriCount": { + "message": "Ιστοσελίδα (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": "Η ιστοσελίδα προστέθηκε" + }, + "addWebsite": { + "message": "Προσθήκη ιστοσελίδας" + }, + "deleteWebsite": { + "message": "Διαγραφή ιστοσελίδας" + }, + "defaultLabel": { + "message": "Προεπιλογή ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Εμφάνιση ανιχνεύσεων αντιστοίχισης $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Απόκρυψη ανιχνεύσεων αντιστοίχισης $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Αυτόματη συμπλήρωση κατά τη φόρτωση της σελίδας;" + }, + "cardExpiredTitle": { + "message": "Ληγμένη κάρτα" + }, + "cardExpiredMessage": { + "message": "Εάν την ανανεώσατε, ενημερώστε τα στοιχεία της κάρτας" + }, + "cardDetails": { + "message": "Στοιχεία κάρτας" + }, + "cardBrandDetails": { + "message": "$BRAND$ λεπτομέρειες", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Ενεργοποίηση κινούμενων εικόνων" + }, + "addAccount": { + "message": "Προσθήκη λογαριασμού" + }, + "loading": { + "message": "Φόρτωση" + }, + "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": "Ανάθεση" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Μόνο μέλη του οργανισμού με πρόσβαση σε αυτές τις συλλογές θα είναι σε θέση να δουν τα αντικείμενα." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Μόνο μέλη του οργανισμού με πρόσβαση σε αυτές τις συλλογές θα είναι σε θέση να δουν τα αντικείμενα." + }, + "bulkCollectionAssignmentWarning": { + "message": "Έχετε επιλέξει $TOTAL_COUNT$ αντικείμενα. Δεν μπορείτε να ενημερώσετε τα $READONLY_COUNT$ αντικείμενα επειδή δεν έχετε δικαιώματα επεξεργασίας.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Προσθήκη πεδίου" + }, + "add": { + "message": "Προσθήκη" + }, + "fieldType": { + "message": "Τύπος πεδίου" + }, + "fieldLabel": { + "message": "Ετικέτα πεδίου" + }, + "textHelpText": { + "message": "Χρήση πεδίων κειμένου για δεδομένα όπως ερωτήσεις ασφαλείας" + }, + "hiddenHelpText": { + "message": "Χρήση κρυφών πεδίων για ευαίσθητα δεδομένα όπως ένας κωδικός πρόσβασης" + }, + "checkBoxHelpText": { + "message": "Χρησιμοποιήστε τα πλαίσια επιλογής αν θέλετε να συμπληρώνετε αυτόματα το πλαίσιο επιλογής μιας φόρμας, όπως αυτό της απομνημόνευσης διεύθυνσης ηλ. ταχυδρομείου" + }, + "linkedHelpText": { + "message": "Χρησιμοποιήστε ένα συνδεδεμένο πεδίο όταν αντιμετωπίζετε προβλήματα αυτόματης συμπλήρωσης για μια συγκεκριμένη ιστοσελίδα." + }, + "linkedLabelHelpText": { + "message": "Εισάγετε το html id, name, aria-label, ή placeholder του πεδίου." + }, + "editField": { + "message": "Επεξεργασία πεδίου" + }, + "editFieldLabel": { + "message": "Επεξεργασία $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Διαγραφή $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ προστέθηκε", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Αναδιοργάνωση $LABEL$. Χρησιμοποιήστε τα βελάκια για τη μετακίνηση του αντικειμένου προς τα πάνω ή κάτω.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ μετακινήθηκε πάνω, θέση $INDEX$ από $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Επιλέξτε συλλογές για ανάθεση" + }, + "personalItemTransferWarningSingular": { + "message": "1 αντικείμενο θα μεταφερθεί μόνιμα στον επιλεγμένο οργανισμό. Δε θα κατέχετε πλέον αυτό το αντικείμενο." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ αντικείμενα θα μεταφερθούν μόνιμα στον επιλεγμένο οργανισμό. Δε θα κατέχετε πλέον αυτά τα αντικείμενα.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 αντικείμενο θα μεταφερθεί μόνιμα στο $ORG$. Δε θα κατέχετε πλέον αυτό το αντικείμενο.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ αντικείμενα θα μεταφερθούν μόνιμα στο $ORG$. Δε θα κατέχετε πλέον αυτά τα αντικείμενα.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Επιτυχής ανάθεση συλλογών" + }, + "nothingSelected": { + "message": "Δεν έχετε επιλέξει τίποτα." + }, + "movedItemsToOrg": { + "message": "Τα επιλεγμένα αντικείμενα μετακινήθηκαν στο $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Τα αντικείμενα μεταφέρθηκαν στο $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Το αντικείμενο μεταφέρθηκε στο $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ μετακινήθηκε κάνω, θέση $INDEX$ από $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Τοποθεσία Αντικειμένου" + }, + "fileSends": { + "message": "Send αρχείων" + }, + "textSends": { + "message": "Send κειμένων" + }, + "bitwardenNewLook": { + "message": "Το Bitwarden έχει μια νέα εμφάνιση!" + }, + "bitwardenNewLookDesc": { + "message": "Είναι πιο ευκολότερο και πιο διαισθητικό από ποτέ στην αυτόματη συμπλήρωση και αναζήτηση από την καρτέλα Θησαυ/κιο. Ρίξτε μια ματιά τριγύρω!" + }, + "accountActions": { + "message": "Ενέργειες λογαριασμού" + }, + "showNumberOfAutofillSuggestions": { + "message": "Εμφάνιση αριθμού προτάσεων αυτόματης συμπλήρωσης σύνδεσης στο εικονίδιο επέκτασης" + }, + "systemDefault": { + "message": "Προεπιλογή συστήματος" + }, + "enterprisePolicyRequirementsApplied": { + "message": "Οι απαιτήσεις της πολιτικής για επιχειρήσεις έχουν εφαρμοστεί σε αυτήν τη ρύθμιση" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Αντικείμενα στον κάδο" + }, + "noItemsInTrash": { + "message": "Κανένα αντικείμενο στον κάδο" + }, + "noItemsInTrashDesc": { + "message": "Τα αντικείμενα που διαγράφετε θα εμφανιστούν εδώ και θα διαγραφούν οριστικά μετά από 30 ημέρες" + }, + "trashWarning": { + "message": "Αντικείμενα που βρίσκονται στον κάδο για περισσότερο από 30 ημέρες θα διαγράφονται αυτόματα" + }, + "restore": { + "message": "Επαναφορά" + }, + "deleteForever": { + "message": "Διαγραφή για πάντα" + }, + "noEditPermissions": { + "message": "Δεν έχετε δικαίωμα να επεξεργαστείτε αυτό το αντικείμενο" } } diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 4030e6c94e8..47e89dcb44a 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "Create account" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "Log in" - }, "enterpriseSingleSignOn": { "message": "Enterprise single sign-on" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Master password hint (optional)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "Tab" }, @@ -107,17 +113,32 @@ "copySecurityCode": { "message": "Copy security code" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { - "message": "Auto-fill" + "message": "Autofill" }, "autoFillLogin": { - "message": "Auto-fill login" + "message": "Autofill login" }, "autoFillCard": { - "message": "Auto-fill card" + "message": "Autofill card" }, "autoFillIdentity": { - "message": "Auto-fill identity" + "message": "Autofill identity" }, "generatePasswordCopied": { "message": "Generate password (copied)" @@ -150,7 +171,7 @@ "message": "Log in to your vault" }, "autoFillInfo": { - "message": "There are no logins available to auto-fill for the current browser tab." + "message": "There are no logins available to autofill for the current browser tab." }, "addLogin": { "message": "Add a login" @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "Minimum password length" }, "uppercase": { - "message": "Uppercase (A-Z)" + "message": "Uppercase (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Lowercase (a-z)" + "message": "Lowercase (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numbers (0-9)" + "message": "Numbers (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Special characters (!@#$%^&*)" + "message": "Special characters (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Number of words" @@ -376,7 +455,12 @@ "message": "Minimum special" }, "avoidAmbChar": { - "message": "Avoid ambiguous characters" + "message": "Avoid ambiguous characters", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Search vault" @@ -415,7 +499,7 @@ "message": "Item added to favorites" }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "Item removed from favorites" }, "notes": { "message": "Notes" @@ -556,6 +640,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -613,7 +718,7 @@ } }, "autofillError": { - "message": "Unable to auto-fill the selected item on this page. Copy and paste the information instead." + "message": "Unable to autofill the selected item on this page. Copy and paste the information instead." }, "totpCaptureError": { "message": "Unable to scan QR code from the current webpage" @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Your login session has expired." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "New URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "Item added" }, @@ -737,23 +873,32 @@ "enableAddLoginNotification": { "message": "Ask to add login" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "Ask to add an item if one isn't found in your vault." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Show cards on Tab page" }, "showCardsCurrentTabDesc": { - "message": "List card items on the Tab page for easy auto-fill." + "message": "List card items on the Tab page for easy autofill." + }, + "showIdentitiesInVaultView": { + "message": "Show identities as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" }, "showIdentitiesCurrentTabDesc": { - "message": "List identity items on the Tab page for easy auto-fill." + "message": "List identity items on the Tab page for easy autofill." }, "clearClipboard": { "message": "Clear clipboard", @@ -791,7 +936,7 @@ "message": "Update" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Unlock" @@ -810,10 +955,10 @@ }, "defaultUriMatchDetection": { "message": "Default URI match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { - "message": "Choose the default way that URI match detection is handled for logins when performing actions such as auto-fill." + "message": "Choose the default way that URI match detection is handled for logins when performing actions such as autofill." }, "theme": { "message": "Theme" @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a Premium member!" }, "premiumCurrentMemberThanks": { "message": "Thank you for supporting Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "All for just $PRICE$ /year!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Refresh complete" }, @@ -1028,7 +1191,7 @@ "message": "Copy TOTP automatically" }, "disableAutoTotpCopyDesc": { - "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login." + "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you autofill the login." }, "enableAutoBiometricsPrompt": { "message": "Ask for biometrics on launch" @@ -1144,10 +1307,10 @@ "selfHostedBaseUrlHint": { "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" }, - "selfHostedCustomEnvHeader" :{ + "selfHostedCustomEnvHeader": { "message": "For advanced configuration, you can specify the base URL of each service independently." }, - "selfHostedEnvFormInvalid" :{ + "selfHostedEnvFormInvalid": { "message": "You must add either the base Server URL or at least one custom environment." }, "customEnvironment": { @@ -1178,14 +1341,23 @@ "message": "Environment URLs saved" }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,38 +1371,57 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "enableAutoFillOnPageLoadDesc": { - "message": "If a login form is detected, auto-fill when the web page loads." + "message": "If a login form is detected, autofill when the web page loads." + }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Compromised or untrusted websites can exploit autofill on page load." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" }, "learnMoreAboutAutofill": { - "message": "Learn more about auto-fill" + "message": "Learn more about autofill" }, "defaultAutoFillOnPageLoad": { "message": "Default autofill setting for login items" }, "defaultAutoFillOnPageLoadDesc": { - "message": "You can turn off auto-fill on page load for individual login items from the item's Edit view." + "message": "You can turn off autofill on page load for individual login items from the item's Edit view." }, "itemAutoFillOnPageLoad": { - "message": "Auto-fill on page load (if set up in Options)" + "message": "Autofill on page load (if set up in Options)" }, "autoFillOnPageLoadUseDefault": { "message": "Use default setting" }, "autoFillOnPageLoadYes": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "autoFillOnPageLoadNo": { - "message": "Do not auto-fill on page load" + "message": "Do not autofill on page load" }, "commandOpenPopup": { "message": "Open vault popup" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Open vault in sidebar" }, - "commandAutofillDesc": { - "message": "Auto-fill the last used login for the current website" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Generate and copy a new random password to the clipboard" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Linked", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1453,7 +1653,7 @@ "typeIdentity": { "message": "Identity" }, - "newItemHeader":{ + "newItemHeader": { "message": "New $TYPE$", "placeholders": { "type": { @@ -1462,7 +1662,7 @@ } } }, - "editItemHeader":{ + "editItemHeader": { "message": "Edit $TYPE$", "placeholders": { "type": { @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -1533,6 +1742,10 @@ "message": "Base domain", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domain name", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Match detection", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Default match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Toggle options" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "There are no passwords to list." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Remove" }, @@ -1677,12 +1899,35 @@ "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" }, "lock": { "message": "Lock", - "description": "Verb form: to make secure or inaccesible by" + "description": "Verb form: to make secure or inaccessible by" }, "trash": { "message": "Trash", @@ -1716,16 +1961,16 @@ "message": "Timeout action confirmation" }, "autoFillAndSave": { - "message": "Auto-fill and save" + "message": "Autofill and save" }, "fillAndSave": { "message": "Fill and save" }, "autoFillSuccessAndSavedUri": { - "message": "Item auto-filled and URI saved" + "message": "Item autofilled and URI saved" }, "autoFillSuccess": { - "message": "Item auto-filled " + "message": "Item autofilled " }, "insecurePageWarning": { "message": "Warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page." @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1832,10 +2077,10 @@ "ok": { "message": "Ok" }, - "errorRefreshingAccessToken":{ + "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, - "errorRefreshingAccessTokenDesc":{ + "errorRefreshingAccessTokenDesc": { "message": "No refresh token or API keys found. Please try logging out and logging back in." }, "desktopSyncVerificationTitle": { @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ is not a valid domain", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Password protected" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Copy Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send created", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2200,7 +2500,7 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, - "verificationRequired" : { + "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." }, @@ -2606,10 +2906,10 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { - "message": "How to auto-fill" + "message": "How to autofill" }, "autofillSelectInfoWithCommand": { "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", @@ -2627,16 +2927,25 @@ "message": "Got it" }, "autofillSettings": { - "message": "Auto-fill settings" + "message": "Autofill settings" + }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,30 +3225,38 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" }, - "partialUsername" : { + "partialUsername": { "message": "Partial username", "description": "Screen reader text for when a login item is focused where a partial username is displayed. SR will announce this phrase before reading the text of the partial username" }, @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,10 +3908,28 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, - "addAttachment":{ + "addAttachment": { "message": "Add attachment" }, "maxFileSizeSansPunctuation": { @@ -3559,6 +3965,96 @@ "filters": { "message": "Filters" }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, "cardDetails": { "message": "Card details" }, @@ -3570,5 +4066,277 @@ "example": "Visa" } } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 7f034bab103..a8efa278e58 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "Create account" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "Log in" - }, "enterpriseSingleSignOn": { "message": "Enterprise single sign-on" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Master password hint (optional)" }, + "joinOrganization": { + "message": "Join organisation" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organisation by setting a master password." + }, "tab": { "message": "Tab" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Copy security code" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "Auto-fill" }, @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "Minimum password length" }, "uppercase": { - "message": "Uppercase (A-Z)" + "message": "Uppercase (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Lowercase (a-z)" + "message": "Lowercase (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numbers (0-9)" + "message": "Numbers (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Special characters (!@#$%^&*)" + "message": "Special characters (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Number of words" @@ -376,7 +455,12 @@ "message": "Minimum special" }, "avoidAmbChar": { - "message": "Avoid ambiguous characters" + "message": "Avoid ambiguous characters", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Search vault" @@ -556,6 +640,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Your login session has expired." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "New URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "Item added" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Ask to add login" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "Ask to add an item if one isn't found in your vault." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Show cards on Tab page" }, "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy auto-fill." }, + "showIdentitiesInVaultView": { + "message": "Show identities as Autofill suggestions on Vault view" + }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Default URI match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Choose the default way that URI match detection is handled for logins when performing actions such as auto-fill." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, + "premiumSignUpEmergency": { + "message": "Emergency access" + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a Premium member!" }, "premiumCurrentMemberThanks": { "message": "Thank you for supporting Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to premium and receive:" + }, "premiumPrice": { "message": "All for just $PRICE$ /year!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Refresh complete" }, @@ -1179,9 +1342,18 @@ }, "showAutoFillMenuOnFormFields": { "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { @@ -1202,15 +1374,34 @@ "message": "When auto-fill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { - "message": "Auto-fill on page load" + "message": "Enable Auto-fill on Page Load" }, "enableAutoFillOnPageLoadDesc": { "message": "If a login form is detected, auto-fill when the web page loads." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Compromised or untrusted websites can exploit auto-fill on page load." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" + }, "learnMoreAboutAutofill": { "message": "Learn more about auto-fill" }, @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Open vault in sidebar" }, - "commandAutofillDesc": { - "message": "Auto-fill the last used login for the current website." + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Generate and copy a new random password to the clipboard." @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Linked", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -1533,6 +1742,10 @@ "message": "Base domain", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domain name", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Match detection", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Default match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Toggle options" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "There are no passwords to list." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Remove" }, @@ -1677,6 +1899,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" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organisation policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ is not a valid domain", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Password protected" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Copy Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send created", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrolment" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Auto-fill settings" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" + }, "autofillShortcut": { "message": "Auto-fill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2911,10 +3240,18 @@ "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,6 +3659,30 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Centre?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,20 +3736,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organisation items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organisation items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3415,7 +3803,7 @@ }, "autofillTitle": { "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organisations cannot be accessed. Contact your organisation owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "message": "Auto-fill 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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organisation members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organisation members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to auto-fill a form's checkbox, like a reminder email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing auto-fill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organisation. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organisation. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in the bin" + }, + "noItemsInTrash": { + "message": "No items in the bin" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in the bin more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index f6f7f6dbf2f..c90115ee537 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "Create account" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "Log in" - }, "enterpriseSingleSignOn": { "message": "Enterprise single sign-on" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Master password hint (optional)" }, + "joinOrganization": { + "message": "Join organisation" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organisation by setting a master password." + }, "tab": { "message": "Tab" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Copy security code" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Aadhaar number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy licence number" + }, "autoFill": { "message": "Auto-fill" }, @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "Minimum password length" }, "uppercase": { - "message": "Uppercase (A-Z)" + "message": "Uppercase (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Lowercase (a-z)" + "message": "Lowercase (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numbers (0-9)" + "message": "Numbers (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Special Characters (!@#$%^&*)" + "message": "Special Characters (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Number of words" @@ -376,7 +455,12 @@ "message": "Minimum special" }, "avoidAmbChar": { - "message": "Avoid ambiguous characters" + "message": "Avoid ambiguous characters", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Search vault" @@ -556,6 +640,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Your login session has expired." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "New URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "Added item" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Ask to add login" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "The \"add login notification\" automatically prompts you to save new logins to your vault whenever you log into them for the first time." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Show cards on Tab page" }, "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy auto-fill." }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" + }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Default URI match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Choose the default way that URI match detection is handled for logins when performing actions such as auto-fill." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, + "premiumSignUpEmergency": { + "message": "Emergency access" + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a premium member!" }, "premiumCurrentMemberThanks": { "message": "Thank you for supporting Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to premium and receive:" + }, "premiumPrice": { "message": "All for just $PRICE$ /year!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Refresh complete" }, @@ -1179,9 +1342,18 @@ }, "showAutoFillMenuOnFormFields": { "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { @@ -1202,15 +1374,34 @@ "message": "When auto-fill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "Enable auto-fill on page load" }, "enableAutoFillOnPageLoadDesc": { "message": "If a login form is detected, automatically perform an auto-fill when the web page loads." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Compromised or untrusted websites can exploit auto-fill on page load." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" + }, "learnMoreAboutAutofill": { "message": "Learn more about auto-fill" }, @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Open vault in sidebar" }, - "commandAutofillDesc": { - "message": "Auto-fill the last used login for the current website." + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Generate and copy a new random password to the clipboard." @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Linked", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -1533,6 +1742,10 @@ "message": "Base domain", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domain name", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Match detection", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Default match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Toggle options" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "There are no passwords to list." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Remove" }, @@ -1677,6 +1899,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" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organisation policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Excluded Domains" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ is not a valid domain", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Password protected" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Copy Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Created Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Edited Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Email Verification Required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic Enrollment" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Auto-fill settings" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" + }, "autofillShortcut": { "message": "Auto-fill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2911,10 +3240,18 @@ "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,6 +3659,30 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Centre?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,20 +3736,6 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organisation items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organisation items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Auto-fill suggestions" }, @@ -3415,7 +3803,7 @@ }, "autofillTitle": { "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organisations cannot be accessed. Contact your organisation owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "message": "Auto-fill 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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organisation members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organisation members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to auto-fill a form's checkbox, like a reminder email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing auto-fill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organisation. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organisation. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in bin" + }, + "noItemsInTrash": { + "message": "No items in bin" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in the bin more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index bacb8f106dc..572372bafc2 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Identifícate o crea una nueva cuenta para acceder a tu caja fuerte." }, + "inviteAccepted": { + "message": "Invitación aceptada" + }, "createAccount": { "message": "Crear cuenta" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Termina de crear tu cuenta estableciendo una contraseña" }, - "login": { - "message": "Iniciar sesión" - }, "enterpriseSingleSignOn": { "message": "Inicio de sesión único empresarial" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Pista de contraseña maestra (opcional)" }, + "joinOrganization": { + "message": "Incorporarse a la organización" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Termine de unirse a esta organización estableciendo una contraseña maestra." + }, "tab": { "message": "Pestaña" }, @@ -107,8 +113,23 @@ "copySecurityCode": { "message": "Copiar código de seguridad" }, + "copyName": { + "message": "Copiar nombre" + }, + "copyCompany": { + "message": "Copiar empresa" + }, + "copySSN": { + "message": "Copiar número de seguro social" + }, + "copyPassportNumber": { + "message": "Copiar número de pasaporte" + }, + "copyLicenseNumber": { + "message": "Copiar número de licencia" + }, "autoFill": { - "message": "Autorellenar" + "message": "Autorrellenar" }, "autoFillLogin": { "message": "Autocompletar inicio de sesión" @@ -150,7 +171,7 @@ "message": "Inicia sesión en tu caja fuerte" }, "autoFillInfo": { - "message": "No hay entradas disponibles para autorellenar en la pestaña actual del navegador." + "message": "No hay entradas disponibles para autorrellenar en la pestaña actual del navegador." }, "addLogin": { "message": "Añadir entrada" @@ -280,6 +301,24 @@ "editFolder": { "message": "Editar carpeta" }, + "newFolder": { + "message": "Carpeta nueva" + }, + "folderName": { + "message": "Nombre de carpeta" + }, + "folderHintText": { + "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + }, + "noFoldersAdded": { + "message": "Ninguna carpeta añadida" + }, + "createFoldersToOrganize": { + "message": "Create folders to organize your vault items" + }, + "deleteFolderPermanently": { + "message": "¿Confirma que quiere eliminar permanentemente esta carpeta?" + }, "deleteFolder": { "message": "Eliminar carpeta" }, @@ -345,16 +384,56 @@ "message": "Longitud mínima de contraseña" }, "uppercase": { - "message": "Mayúsculas (A-Z)" + "message": "Mayúsculas (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Minúsculas (a-z)" + "message": "Minúsculas (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Números (0-9)" + "message": "Números (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Caracteres especiales (!@#$%^&*)" + "message": "Caracteres especiales (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Incluir", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Incluir letras mayúsculas", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Incluir letras minúsculas", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Incluir números", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Incluir caracteres especiales", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Número de palabras" @@ -376,7 +455,12 @@ "message": "Mínimo de caracteres especiales" }, "avoidAmbChar": { - "message": "Evitar caracteres ambiguos" + "message": "Evitar caracteres ambiguos", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Evitar caracteres ambiguos", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Buscar en caja fuerte" @@ -556,6 +640,18 @@ "security": { "message": "Seguridad" }, + "confirmMasterPassword": { + "message": "Confirmar contraseña maestra" + }, + "masterPassword": { + "message": "Contraseña maestra" + }, + "masterPassImportant": { + "message": "¡Tu contraseña maestra no se puede recuperar si la olvidas!" + }, + "masterPassHintLabel": { + "message": "Pista de la contraseña maestra" + }, "errorOccurred": { "message": "Ha ocurrido un error" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "¡Tu nueva cuenta ha sido creada! Ahora puedes acceder." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "Accedió correctamente a su cuenta" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Código de verificación requerido." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Código de verificación no válido" }, @@ -613,7 +718,7 @@ } }, "autofillError": { - "message": "No se ha podido autorellenar la entrada seleccionada en esta página. Copia/pega tu usuario y/o contraseña." + "message": "No se ha podido autorrellenar la entrada seleccionada en esta página. Copia/pega tu usuario y/o contraseña." }, "totpCaptureError": { "message": "No se puede escanear el código QR desde la página web actual" @@ -624,6 +729,18 @@ "totpCapture": { "message": "Escanee el código QR del autenticador desde la página web actual" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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": "Copiar clave de autenticador (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Tu sesión ha expirado." }, + "logIn": { + "message": "Acceder" + }, + "restartRegistration": { + "message": "Reiniciar registro" + }, + "expiredLink": { + "message": "Enlace caducado" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Reinicie el registro o pruebe a acceder a su cuenta." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Puede que ya tenga una cuenta" + }, "logOutConfirmation": { "message": "¿Estás seguro de querer cerrar la sesión?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Nueva URI" }, + "addDomain": { + "message": "Añadir dominio", + "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": { "message": "Elemento añadido" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Pedir que se añada el inicio de sesión" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "La opción \"Notificación para añadir entradas\" pregunta automáticamente si quieres guardar nuevas entradas en tu caja fuerte cuando te identificas en un sitio web por primera vez." }, "addLoginNotificationDescAlt": { "message": "Pide que se agregue un elemento si no se encuentra uno en su caja fuerte. Se aplica a todas las cuentas que hayan iniciado sesión." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Mostrar las tarjetas en la pestaña" }, "showCardsCurrentTabDesc": { "message": "Listar los elementos de tarjetas en la página para facilitar el auto-rellenado." }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" + }, "showIdentitiesCurrentTab": { "message": "Mostrar las identidades en la página" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Detección por defecto de coincidencia de URI", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Elija el método de detección por defecto de coincidencia de URI que se utilizará para acciones de inicio de sesión como autorrellenado." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB de espacio cifrado en disco para adjuntos." }, + "premiumSignUpEmergency": { + "message": "Acceso de emergencia." + }, "premiumSignUpTwoStepOptions": { "message": "Opciones de inicio de sesión con autenticación de dos pasos propietarios como YubiKey y Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Puedes comprar la membresía Premium en la caja fuerte web de bitwarden.com. ¿Quieres visitar el sitio web ahora?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "¡Eres un miembro Premium!" }, "premiumCurrentMemberThanks": { "message": "Gracias por apoyar el desarrollo de Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "¡Todo por solo %price% /año!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Actualización completada" }, @@ -1179,10 +1342,19 @@ }, "showAutoFillMenuOnFormFields": { "message": "Mostrar menú de autocompletar en los campos del formulario", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { - "message": "Se aplica a todas las cuentas que hayan iniciado sesión." + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { + "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { "message": "Desactive la configuración del gestor de contraseñas del navegador para evitar conflictos." @@ -1202,15 +1374,34 @@ "message": "Cuando se seleccione el icono de relleno automático", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "Habilitar autorrellenar al cargar la página" }, "enableAutoFillOnPageLoadDesc": { "message": "Si se detecta un formulario, realizar automáticamente un autorellenado cuando la web cargue." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Los sitios web vulnerados o no confiables pueden explotar el autorelleno al cargar la página." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" + }, "learnMoreAboutAutofill": { "message": "Más información sobre el relleno automático" }, @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Abrir caja fuerte en la barra lateral" }, - "commandAutofillDesc": { - "message": "Autorrellenar la última entrada utilizada para la página actual." + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Generar y copiar una nueva contraseña aleatoria al portapapeles." @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Booleano" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Vinculado", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "Ver $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Historial de contraseñas" }, @@ -1533,6 +1742,10 @@ "message": "Dominio base", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Nombre de dominio", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Tipo de detección", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Detección por defecto", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Alternar opciones" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "No hay contraseñas que listar." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Eliminar" }, @@ -1677,6 +1899,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Una o más políticas de la organización están afectando la configuración del generador" }, + "passwordGenerator": { + "message": "Generador de contraseñas" + }, + "usernameGenerator": { + "message": "Generador de nombres de usuario" + }, + "useThisPassword": { + "message": "Usar esta contraseña" + }, + "useThisUsername": { + "message": "Usar este nombre de usuario" + }, + "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" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Su nueva contraseña maestra no cumple con los requisitos de la política." }, - "receiveMarketingEmails": { - "message": "Obtén correos electrónicos de Bitwarden para anuncios, consejos y oportunidades de investigación." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Cancelar suscripción" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Por favor, desbloquea a este usuario en la aplicación de escritorio e inténtalo de nuevo." }, + "biometricsNotAvailableTitle": { + "message": "Desbloqueo biométrico no disponible" + }, + "biometricsNotAvailableDesc": { + "message": "El desbloqueo biométrico no está disponible actualmente. Inténtelo de nuevo más tarde." + }, "biometricsFailedTitle": { "message": "Fallo de biométrica" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "Una política organizacional ha bloqueado la importación de elementos a su caja fuerte personal." }, + "domainsTitle": { + "message": "Dominios", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Dominios excluidos" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden no pedirá que se guarden los datos de acceso para estos dominios en todas las sesiones iniciadas. Debe actualizar la página para que los cambios surtan efecto." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ no es un dominio válido", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Protegido por contraseña" }, + "copyLink": { + "message": "Copiar enlace" + }, "copySendLink": { "message": "Copiar enlace Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Envío creado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Envío editado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Verificación de correo electrónico requerida" }, + "emailVerifiedV2": { + "message": "Correo electrónico verificado" + }, "emailVerificationRequiredDesc": { "message": "Debes verificar tu correo electrónico para usar esta función. Puedes verificar tu correo electrónico en la caja fuerte web." }, @@ -2179,6 +2476,9 @@ "updateWeakMasterPasswordWarning": { "message": "Su contraseña maestra no cumple con una o más de las políticas de su organización. Para acceder a la caja fuerte, debe actualizar su contraseña maestra ahora. Proceder le desconectará de su 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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Inscripción automática" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Ajustes de autocompletar" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Cambiar atajo" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Gestionar atajos" + }, "autofillShortcut": { "message": "Atajo de teclado para autocompletar" }, - "autofillShortcutNotSet": { - "message": "El atajo de autocompletar no está establecido. Cambie esto en los ajustes del navegador." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "El atajo de autocompletar es $COMMAND$. Cambie esto en los ajustes del navegador.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Dispositivo de confianza" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Entrada requerida." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Seleccione --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Los elementos que requieren la contraseña maestra no se pueden rellenar automáticamente al cargar la página. Se desactivó el autorrellenado de la página.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "El autorrellenado de la página está usando la configuración predeterminada.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Desactiva la solicitud de contraseña maestra para editar este campo", @@ -2911,10 +3240,18 @@ "message": "Desbloquea tu cuenta para ver las entradas coincidentes", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Desbloquear la cuenta", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Rellenar credenciales para", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Añadir elemento de caja fuerte nuevo", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "Identidad nueva", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Menú de relleno automático de Bitwarden disponible. Presione ↓ para seleccionar.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error al conectarse con el servicio Duo. Utiliza un método de inicio de sesión en dos pasos diferente o ponte en contacto con Duo para obtener ayuda." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Abra Duo y siga los pasos para terminar de iniciar sesión." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Contraseña de archivo no válida. Por favor utilice la contraseña que introdujo cuando creó el archivo de exportación." }, - "importDestination": { - "message": "Importar destino" + "destination": { + "message": "Destino" }, "learnAboutImportOptions": { "message": "Aprende sobre tus opciones de importación" @@ -3122,8 +3486,8 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verificación requerida por el sitio inicial. Esta característica aún no está implementada para cuentas sin contraseña maestra." }, - "logInWithPasskey": { - "message": "¿Iniciar sesión con clave de acceso?" + "logInWithPasskeyQuestion": { + "message": "Log in with passkey?" }, "passkeyAlreadyExists": { "message": "Ya existe una clave de acceso para esta aplicación." @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "No tiene un inicio de sesión que coincida para este sitio." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirmar" }, @@ -3143,8 +3510,11 @@ "savePasskeyNewLogin": { "message": "Guardar clave de acceso como nuevo inicio de sesión" }, - "choosePasskey": { - "message": "Elija un inicio de sesión para guardar esta clave de acceso" + "chooseCipherForPasskeySave": { + "message": "Choose a login to save this passkey to" + }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" }, "passkeyItem": { "message": "Elemento de clave de acceso" @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,6 +3659,30 @@ "message": "Formatos comunes", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "¿Hacer de Bitwarden su administrador de contraseñas predeterminado?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" @@ -3317,10 +3711,18 @@ "message": "¡Credenciales guardadas con éxito!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "¡Credenciales actualizadas con éxito!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Se produjo un error al guardar las credenciales. Revise la consola para obtener detalles.", "description": "Notification message for when saving credentials has failed." @@ -3334,20 +3736,6 @@ "passkeyRemoved": { "message": "Clave de acceso eliminada" }, - "unassignedItemsBannerNotice": { - "message": "Aviso: Los elementos de organización no asignados ya no son visibles en la vista de Todas las cajas fuertes y solo son accesibles a través de la Consola de Administrador." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Aviso: El 16 de mayo de 2024, los elementos de organización no asignados no serán visibles en la vista de Todas las cajas fuertes y solo serán accesibles a través de la Consola de Administrador." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Asignar estos elementos a una colección de", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "para hcerlos visibles.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Autocompletar sugerencias" }, @@ -3415,7 +3803,7 @@ }, "autofillTitle": { "message": "Autocompletar - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No hay valores para copiar" }, - "assignCollections": { - "message": "Asignar colecciones" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copiar correo electrónico" @@ -3493,13 +3881,13 @@ "message": "Elementos sin carpeta" }, "itemDetails": { - "message": "Item details" + "message": "Detalles del elemento" }, "itemName": { - "message": "Item name" + "message": "Nombre del elemento" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "No puedes eliminar colecciones con permisos de solo visualización: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3899,33 @@ "message": "La organización está desactivada" }, "owner": { - "message": "Owner" + "message": "Propietario" }, "selfOwnershipLabel": { - "message": "You", + "message": "Tú", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "No se puede acceder a los elementos de las organizaciones desactivadas. Ponte en contacto con el propietario de tu organización para obtener ayuda." }, + "additionalInformation": { + "message": "Información adicional" + }, + "itemHistory": { + "message": "Historial del elemento" + }, + "lastEdited": { + "message": "Última edición" + }, + "ownerYou": { + "message": "Propietario: Tú" + }, + "linked": { + "message": "Vinculado" + }, + "copySuccessful": { + "message": "Copiado exitosamente" + }, "upload": { "message": "Subir" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filtros" + }, + "personalDetails": { + "message": "Datos personales" + }, + "identification": { + "message": "Identificación" + }, + "contactInfo": { + "message": "Información de contacto" + }, + "downloadAttachment": { + "message": "Descargar - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Datos de la tarjeta" + }, + "cardBrandDetails": { + "message": "Detalles de $BRAND$", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Activar animaciones" + }, + "addAccount": { + "message": "Añadir cuenta" + }, + "loading": { + "message": "Cargando" + }, + "data": { + "message": "Datos" + }, + "passkeys": { + "message": "Passkeys", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Contraseñas", + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Añadir campo" + }, + "add": { + "message": "Añadir" + }, + "fieldType": { + "message": "Tipo de campo" + }, + "fieldLabel": { + "message": "Etiqueta de campo" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Ubicación del elemento" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden tiene un aspecto nuevo." + }, + "bitwardenNewLookDesc": { + "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" + }, + "accountActions": { + "message": "Acciones de cuenta" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Elementos en la papelera" + }, + "noItemsInTrash": { + "message": "Ningún elemento en la papelera" + }, + "noItemsInTrashDesc": { + "message": "Los elementos que elimine aparecerán aquí y se eliminarán permanentemente al cabo de 30 días" + }, + "trashWarning": { + "message": "Los elementos que permanezcan más de 30 días en la papelera se eliminarán de forma automática" + }, + "restore": { + "message": "Restaurar" + }, + "deleteForever": { + "message": "Eliminar para siempre" + }, + "noEditPermissions": { + "message": "No tiene permiso de editar este elemento" } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 415987287c5..947b9b0966c 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -3,30 +3,30 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwardeni paroolihaldur", "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": "Kodus, tööl ja teel - Bitwarden hoiustab imelihtsalt kõik su paroolid, pääsuvõtmed ja tundliku info", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Logi oma olemasolevasse kontosse sisse või loo uus konto." }, + "inviteAccepted": { + "message": "Kutse vastu võetud" + }, "createAccount": { - "message": "Loo konto" + "message": "Konto loomine" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Määra tugev parool" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" - }, - "login": { - "message": "Logi sisse" + "message": "Lõpeta konto loomine parooli luues" }, "enterpriseSingleSignOn": { - "message": "Ettevõtte Single Sign-On" + "message": "Ettevõtte ühekordne sisselogimine" }, "cancel": { "message": "Tühista" @@ -50,7 +50,7 @@ "message": "Vihje võib abiks olla olukorras, kui oled ülemparooli unustanud." }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Kui sa unustad oma parooli, saad saata parooli vihje e-mailile.\n$CURRENT$/$MAXIMUM$ tähepiirang.", "placeholders": { "current": { "content": "$1", @@ -68,17 +68,23 @@ "masterPassHint": { "message": "Ülemparooli vihje (ei ole kohustuslik)" }, + "joinOrganization": { + "message": "Liitu organisatsiooniga" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Lõpeta organisatsiooniga liitumine määrates ülemparool." + }, "tab": { "message": "Kaart" }, "vault": { - "message": "Hoidla" + "message": "Seif" }, "myVault": { - "message": "Minu hoidla" + "message": "Minu seif" }, "allVaults": { - "message": "Kõik hoidlad" + "message": "Kõik seifid" }, "tools": { "message": "Tööriistad" @@ -107,14 +113,29 @@ "copySecurityCode": { "message": "Kopeeri turvakood" }, + "copyName": { + "message": "Kopeeri nimi" + }, + "copyCompany": { + "message": "Kopeeri firma nimi" + }, + "copySSN": { + "message": "Kopeeri isikukood" + }, + "copyPassportNumber": { + "message": "Kopeeri passi number" + }, + "copyLicenseNumber": { + "message": "Kopeeri litsentsi number" + }, "autoFill": { "message": "Automaatne täitmine" }, "autoFillLogin": { - "message": "Täida konto andmed" + "message": "Täida andmed automaatselt" }, "autoFillCard": { - "message": "Täida kaardi andmed" + "message": "Täida automaatselt kaardi andmed" }, "autoFillIdentity": { "message": "Täida identiteet" @@ -126,7 +147,7 @@ "message": "Kopeeri kohandatud välja nimi" }, "noMatchingLogins": { - "message": "Sobivaid kontoandmeid ei leitud." + "message": "Sobivaid kontoandmeid ei leitud" }, "noCards": { "message": "Kaardid puuduvad" @@ -144,7 +165,7 @@ "message": "Lisa identiteet" }, "unlockVaultMenu": { - "message": "Lukusta hoidla lahti" + "message": "Ava hoidla" }, "loginToVaultMenu": { "message": "Logi hoidlasse sisse" @@ -156,7 +177,7 @@ "message": "Lisa konto andmed" }, "addItem": { - "message": "Lisa kirje" + "message": "Lisa ese" }, "passwordHint": { "message": "Parooli vihje" @@ -189,28 +210,28 @@ "message": "Muuda ülemparooli" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Jätka veebibrauseris?" }, "continueToWebAppDesc": { - "message": "Explore more features of your Bitwarden account on the web app." + "message": "Uuri teisi Bitwardeni konto funktsioone veebirakenduses." }, "continueToHelpCenter": { - "message": "Continue to Help Center?" + "message": "Kas soovid minna Abikeskusesse?" }, "continueToHelpCenterDesc": { - "message": "Learn more about how to use Bitwarden on the Help Center." + "message": "Uuri teisigi Bitwardeni kasutusvõimalusi Abikeskuses." }, "continueToBrowserExtensionStore": { - "message": "Continue to browser extension store?" + "message": "Mine edasi veebilaienduste poodi?" }, "continueToBrowserExtensionStoreDesc": { - "message": "Help others find out if Bitwarden is right for them. Visit your browser's extension store and leave a rating now." + "message": "Aita meil jõuda rohkemate inimesteni. Külasta enda laienduste veebipoodi ja jäta sinna positiivne hinnang." }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Ülemparooli saab muuta Bitwardeni veebirakenduses." }, "fingerprintPhrase": { - "message": "Sõrmejälje fraas", + "message": "Unikaalne sõnajada", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." }, "yourAccountsFingerprint": { @@ -224,43 +245,43 @@ "message": "Logi välja" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "Meist" }, "about": { "message": "Rakenduse info" }, "moreFromBitwarden": { - "message": "More from Bitwarden" + "message": "Rohkem Bitwardeni kohta" }, "continueToBitwardenDotCom": { - "message": "Continue to bitwarden.com?" + "message": "Mine edasi bitwarden.com-i?" }, "bitwardenForBusiness": { - "message": "Bitwarden for Business" + "message": "Bitwarden Ärikliendile" }, "bitwardenAuthenticator": { "message": "Bitwarden Authenticator" }, "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": "Bitwardeni Autentiteerijaga saad sa hoiustada autentiteerimise võtmeid ja luua TOTP koode kaheastmeliseks kinnitamiseks. Uuri lähemalt veebilehelt bitwarden.com" }, "bitwardenSecretsManager": { "message": "Bitwarden Secrets Manager" }, "continueToSecretsManagerPageDesc": { - "message": "Securely store, manage, and share developer secrets with Bitwarden Secrets Manager. Learn more on the bitwarden.com website." + "message": "Hoiusta, halda ja jaga turvaliselt arendajate saladusi läbi Bitwarden Secrets Manageri. Uuri lähemalt veebilehelt 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": "Loo sujuv ja turvaline kogemus sisselogimisel Passwordless.dev-iga ja ilma traditsiooniliste paroolideta. Uuri lähemalt veebilehelt bitwarden.com." }, "freeBitwardenFamilies": { - "message": "Free Bitwarden Families" + "message": "Tasuta Bitwarden Peredele" }, "freeBitwardenFamiliesPageDesc": { - "message": "You are eligible for Free Bitwarden Families. Redeem this offer today in the web app." + "message": "Sul on võimalik saada endale tasuta Bitwarden Families plaan. Lunasta see pakkumine meie veebirakenduses." }, "version": { "message": "Versioon" @@ -280,6 +301,24 @@ "editFolder": { "message": "Muuda kausta" }, + "newFolder": { + "message": "Uus kaust" + }, + "folderName": { + "message": "Kausta nimi" + }, + "folderHintText": { + "message": "Kausta teise kasuta panemiseks lisa sihtkausta nimi, millele järgneb \"/\". Näiteks: Sotsiaalmeedia/Foorumid" + }, + "noFoldersAdded": { + "message": "Ei lisanud ühtegi kausta" + }, + "createFoldersToOrganize": { + "message": "Loo kaustasid, et oma hoidla kirjeid organiseerida" + }, + "deleteFolderPermanently": { + "message": "Kas sa oled kindel, et soovid selle kausta jäädavalt kustutada?" + }, "deleteFolder": { "message": "Kustuta Kaust" }, @@ -321,7 +360,7 @@ "message": "Loo oma kontodele tugevaid ja unikaalseid paroole." }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "Bitwardeni veebirakendus" }, "importItems": { "message": "Impordi andmed" @@ -345,16 +384,56 @@ "message": "Lühim lubatud parooli pikkus" }, "uppercase": { - "message": "Suurtäht (A-Z) " + "message": "Suurtäht (A-Z) ", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Väiketäht (a-z) " + "message": "Väiketäht (a-z) ", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numbrid (0-9)" + "message": "Numbrid (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Erimärgid (!@#$%^&*)" + "message": "Erimärgid (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Kasuta", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Kasuta trükitähti", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Kasuta kirjatähti", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Kasuta numbreid", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Kasuta sümboleid", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Sõnade arv" @@ -376,7 +455,12 @@ "message": "Vähim arv spetsiaalmärke" }, "avoidAmbChar": { - "message": "Väldi ebamääraseid kirjamärke" + "message": "Väldi ebamääraseid kirjamärke", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Väldi raskesti eristatavaid tähti ja sümboleid", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Otsi hoidlast" @@ -400,7 +484,7 @@ "message": "Parool" }, "totp": { - "message": "Authenticator secret" + "message": "Salajane autentikaatori võti" }, "passphrase": { "message": "Paroolifraas" @@ -409,13 +493,13 @@ "message": "Lemmik" }, "unfavorite": { - "message": "Unfavorite" + "message": "Eemalda lemmikutest" }, "itemAddedToFavorites": { - "message": "Item added to favorites" + "message": "Ese lisatud lemmikutesse" }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "Ese eemaldatud lemmikutest" }, "notes": { "message": "Märkmed" @@ -439,7 +523,7 @@ "message": "Käivita" }, "launchWebsite": { - "message": "Launch website" + "message": "Ava Veebileht" }, "website": { "message": "Veebileht" @@ -454,19 +538,19 @@ "message": "Muu" }, "unlockMethods": { - "message": "Unlock options" + "message": "Avamise valikud" }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Hoidla ajalõpu tegevuse muutmiseks vali esmalt lahtilukustamise meetod." }, "unlockMethodNeeded": { - "message": "Set up an unlock method in Settings" + "message": "Määra avamise meetod seadetes" }, "sessionTimeoutHeader": { - "message": "Session timeout" + "message": "Sessiooni ajalõpp" }, "otherOptions": { - "message": "Other options" + "message": "Muud valikud" }, "rateExtension": { "message": "Hinda seda laiendust" @@ -509,7 +593,7 @@ "message": "Lukusta paroolihoidla" }, "lockAll": { - "message": "Lock all" + "message": "Lukusta kõik" }, "immediately": { "message": "Koheselt" @@ -556,6 +640,18 @@ "security": { "message": "Turvalisus" }, + "confirmMasterPassword": { + "message": "Kinnita ülemparool" + }, + "masterPassword": { + "message": "Ülemparool" + }, + "masterPassImportant": { + "message": "Ülemparooli ei saa taastada, kui sa selle unustama peaksid!" + }, + "masterPassHintLabel": { + "message": "Vihje ülemparoolile" + }, "errorOccurred": { "message": "Ilmnes viga" }, @@ -587,11 +683,17 @@ "newAccountCreated": { "message": "Konto on loodud! Võid nüüd sisse logida." }, + "newAccountCreated2": { + "message": "Uus konto loodud!" + }, + "youHaveBeenLoggedIn": { + "message": "Olete sisse logitud!" + }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Sisselogimine õnnestus" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Võid selle akna sulgeda" }, "masterPassSent": { "message": "Ülemparooli vihje saadeti sinu e-postile." @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Nõutav on kinnituskood." }, + "webauthnCancelOrTimeout": { + "message": "Autentimine tühistati või kestis liiga kaua aega. Palun proovi uuesti." + }, "invalidVerificationCode": { "message": "Vale kinnituskood" }, @@ -616,26 +721,53 @@ "message": "Automaatne täitmine ebaõnnestus. Palun kopeeri informatsioon käsitsi." }, "totpCaptureError": { - "message": "Unable to scan QR code from the current webpage" + "message": "Ei õnnestunud skännida sellelt lehelt QR-kood" }, "totpCaptureSuccess": { - "message": "Authenticator key added" + "message": "Autentimise võti on lisatud" }, "totpCapture": { - "message": "Scan authenticator QR code from current webpage" + "message": "Skänneeri see QR-kood läbi autentikaatori" + }, + "totpHelperTitle": { + "message": "Muuda 2-astmeline kinnitamine sujuvaks" + }, + "totpHelper": { + "message": "Bitwarden saab hoiustada ja täita 2-astmelise kinnitamise koode. Kopeeri ja kleebi võti siia." + }, + "totpHelperWithCapture": { + "message": "Bitwarden saab hoiustada ja täita 2-astmelise kinnitamise koode. Vajuta kaamera ikoonile, et teha ekraanipilt autentiteerimise QR koodist või kopeeri ja kleebi võti siia." + }, + "learnMoreAboutAuthenticators": { + "message": "Uuri lähemalt autentikaatorite kohta" }, "copyTOTP": { - "message": "Copy Authenticator key (TOTP)" + "message": "Kopeeri autentiteerimise võti (TOTP)" }, "loggedOut": { "message": "Välja logitud" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "Sa logisid oma kontolt välja." }, "loginExpired": { "message": "Sessioon on aegunud." }, + "logIn": { + "message": "Logi sisse" + }, + "restartRegistration": { + "message": "Alusta registreerimist uuesti" + }, + "expiredLink": { + "message": "Aegunud link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Palun alusta registreerimist uuesti või proovi sisse logida." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Oled kindel, et soovid välja logida?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Uus URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "Kirje on lisatud" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Küsi \"Lisa konto andmed\"" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "\"Lisa konto andmed\" teavitus ilmub pärast esimest sisselogimist ning võimaldab kontoandmeid automaatselt Bitwardenisse lisada." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Kuva \"Kaart\" vaates kaardiandmed" }, "showCardsCurrentTabDesc": { "message": "Kuvab \"Kaart\" vaates kaardiandmeid, et neid saaks kiiresti sisestada" }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" + }, "showIdentitiesCurrentTab": { "message": "Kuva \"Kaart\" vaates identiteete" }, @@ -779,7 +924,7 @@ "message": "Ask to update a login's password when a change is detected on a website. Applies to all logged in accounts." }, "enableUsePasskeys": { - "message": "Ask to save and use passkeys" + "message": "Küsi luba pääsuvõtmete salvestamiseks ja kasutamiseks" }, "usePasskeysDesc": { "message": "Ask to save new passkeys or log in with passkeys stored in your vault. Applies to all logged in accounts." @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Vaike URI sobivuse tuvastamine", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Vali vaikeviis, kuidas kirje ja URI sobivus tuvastatakse. Seda kasutatakse näiteks siis, kui lehele üritatakse automaatselt andmeid sisestada." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB ulatuses krüpteeritud salvestusruum." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Bitwardeni premium versiooni saab osta bitwarden.com veebihoidlas. Avan veebihoidla?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Oled premium kasutaja!" }, "premiumCurrentMemberThanks": { "message": "Täname, et toetad Bitwardenit." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "Kõik see ainult $PRICE$ / aastas!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Uuendamine lõpetatud" }, @@ -1178,14 +1341,23 @@ "message": "The environment URLs have been saved." }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,18 +1371,37 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "Luba kontoandmete täitmine" }, "enableAutoFillOnPageLoadDesc": { "message": "Sisselogimise vormi tuvastamisel sisestatakse sinna kontoandmed automaatselt." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Häkitud või ebausaldusväärsed veebilehed võivad lehe laadimisel automaatset sisestamist kuritarvitada." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" + }, "learnMoreAboutAutofill": { "message": "Rohkem infot automaattäite kohta" }, @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Ava hoidla küljeribal" }, - "commandAutofillDesc": { - "message": "Sisesta lehele viimati kasutatud kontoandmed." + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Loo ja kopeeri uus juhuslikult koostatud parool lõikelauale." @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Märkeruut" + }, "cfTypeLinked": { "message": "Ühenduses", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1292,7 +1492,7 @@ "message": "Kuvab iga kirje kõrval lehekülje ikooni." }, "faviconDescAlt": { - "message": "Show a recognizable image next to each login. Applies to all logged in accounts." + "message": "Näita väikest tuttavat ikooni iga kirje kõrval. Kehtib ka sisselogitud kontodele." }, "enableBadgeCounter": { "message": "Kuva kirjete arvu" @@ -1454,7 +1654,7 @@ "message": "Identiteet" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "Uus $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Paroolide ajalugu" }, @@ -1481,7 +1690,7 @@ "message": "Kogumikud" }, "nCollections": { - "message": "$COUNT$ collections", + "message": "$COUNT$ kogumikku", "placeholders": { "count": { "content": "$1", @@ -1533,6 +1742,10 @@ "message": "Baasdomeen", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Serveri nimi [base domain] (soovitatav)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domeeni nimi", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Sobivuse tuvastamine", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Vaike sobivuse tuvastamine", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Valik sisse" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Puuduvad paroolid, mida kuvada." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Eemalda" }, @@ -1651,7 +1873,7 @@ "message": "Vale PIN kood." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "Liiga palju ebaõnnestunud katseid. Login välja." }, "unlockWithBiometrics": { "message": "Ava biomeetriaga" @@ -1677,6 +1899,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Organisatsiooni seaded mõjutavad parooli genereerija sätteid." }, + "passwordGenerator": { + "message": "Parooli genereerija" + }, + "usernameGenerator": { + "message": "Kasutajanime genereerija" + }, + "useThisPassword": { + "message": "Kasuta seda parooli" + }, + "useThisUsername": { + "message": "Kasuta seda kasutajanime" + }, + "securePasswordGenerated": { + "message": "Turvaline parool loodud! Ära unusta uuendata seda ka veebisaidil." + }, + "useGeneratorHelpTextPartOne": { + "message": "Kasuta generaatorit", + "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": "et luua tugev ja ainulaadne parool", + "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" }, @@ -1707,7 +1952,7 @@ "message": "Kirje on taastatud" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "On juba konto?" }, "vaultTimeoutLogOutConfirmation": { "message": "Väljalogimine eemaldab hoidlale ligipääsu ning nõuab pärast ajalõpu perioodi uuesti autentimist. Oled kindel, et soovid seda valikut kasutada?" @@ -1719,7 +1964,7 @@ "message": "Täida ja salvesta" }, "fillAndSave": { - "message": "Fill and save" + "message": "Täida ja salvesta" }, "autoFillSuccessAndSavedUri": { "message": "Kirje täideti ja URI salvestati" @@ -1799,20 +2044,20 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Uus ülemparool ei vasta eeskirjades väljatoodud tingimustele." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Soovin saada nõuandeid, uudiseid ja pakkumisi Bitwardenilt oma postkasti." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "Lõpeta tellimus" }, "atAnyTime": { - "message": "at any time." + "message": "iga hetk." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Jätkates nõustud sa" }, "and": { - "message": "and" + "message": "ja" }, "acceptPolicies": { "message": "Märkeruudu markeerimisel nõustud järgnevaga:" @@ -1833,10 +2078,10 @@ "message": "Ok" }, "errorRefreshingAccessToken": { - "message": "Access Token Refresh Error" + "message": "Juurdepääsukoodi Värskendamine Ebaõnnestus" }, "errorRefreshingAccessTokenDesc": { - "message": "No refresh token or API keys found. Please try logging out and logging back in." + "message": "Ei leidnud värskendamise koodi või API võtit. Palun proovi logida välja ja uuesti sisse." }, "desktopSyncVerificationTitle": { "message": "Töölaua sünkroonimise kinnitamine" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biomeetria nurjus" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Väljajäetud domeenid" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ ei ole õige domeen.", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Parooliga kaitstud" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Kopeeri Sendi link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send on loodud", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Muudetud", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Vajalik on e-posti kinnitamine" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Selle funktsiooni kasutamiseks pead kinnitama oma e-posti aadressi. Saad seda teha veebihoidlas." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automaatne liitumine" }, @@ -2558,10 +2858,10 @@ "message": "Bitwardeni rakenduse seadistuses peab olema konfigureeritud sisselogimine läbi seadme. Vajad teist valikut?" }, "fingerprintPhraseHeader": { - "message": "Sõrmejälje fraas" + "message": "Unikaalne sõnajada" }, "fingerprintMatchInfo": { - "message": "Veendu, et hoidla on lahti lukustatud ja sõrmejälje fraasid seadmete vahel ühtivad." + "message": "Veendu, et hoidla on lahti lukustatud ja unikaalne sõnajada ühtib teiste seadmetega." }, "resendNotification": { "message": "Saada märguanne uuesti" @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Automaattäite seaded" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" + }, "autofillShortcut": { "message": "Automaattäite klaviatuuri otseteed" }, - "autofillShortcutNotSet": { - "message": "Automaattäite otsetee pole määratud. Muuda seda brauseri seadetes." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "Automaattäite otsetee on: $COMMAND$. Saad seda brauseri seadetes muuta.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Seade on usaldusväärne" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Sisestus on nõutav." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Vali --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domeen" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Pääsuvõti on eemaldatud" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 1f1c9e68dba..1cbb17bcf61 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Saioa hasi edo sortu kontu berri bat zure kutxa gotorrera sartzeko." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "Sortu kontua" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "Hasi saioa" - }, "enterpriseSingleSignOn": { "message": "Enpresentzako saio hasiera bakarra" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Pasahitz nagusirako pista (aukerakoa)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "Fitxak" }, @@ -107,11 +113,26 @@ "copySecurityCode": { "message": "Kopiatu segurtasun-kodea" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "Auto-betetzea" }, "autoFillLogin": { - "message": "Auto-fill login" + "message": "Autofill login" }, "autoFillCard": { "message": "Auto-bete txartela" @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "Pasahitzaren gutxieneko luzera" }, "uppercase": { - "message": "Letra larria (A-Z)" + "message": "Letra larria (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Letra txikia (a-z)" + "message": "Letra txikia (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Zenbakiak (0-9)" + "message": "Zenbakiak (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Karaktere bereziak (!@#$%^&*)" + "message": "Karaktere bereziak (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Hitz kopurua" @@ -376,7 +455,12 @@ "message": "Gutxieneko karaktere bereziak" }, "avoidAmbChar": { - "message": "Saihestu karaktere anbiguoak" + "message": "Saihestu karaktere anbiguoak", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Bilatu kutxa gotorrean" @@ -556,6 +640,18 @@ "security": { "message": "Segurtasuna" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Akats bat gertatu da" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Zure kontua egina dago. Orain saioa has dezakezu." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Egiaztatze-kodea behar da." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Egiaztatze-kodea ez da baliozkoa" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Saioa amaitu da." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Ziur zaude saioa itxi nahi duzula?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "URI berria" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "Elementua gehituta" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Galdetu saio-hasiera gehitzeko" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "Elementu bat gehitu nahi duzun galdetu, elementu hau zure kutxa gotorrean ez badago." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Erakutsi txartelak fitxa orrian" }, "showCardsCurrentTabDesc": { "message": "Erakutsi elementuen txartelak fitxa orrian, erraz auto-betetzeko." }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" + }, "showIdentitiesCurrentTab": { "message": "Erakutsi identitateak fitxa orrian" }, @@ -791,7 +936,7 @@ "message": "Eguneratu" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Unlock" @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Lehenetsitako detekzioa URI kointzidentziarako", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Hautatu auto-betetzea bezalako saio-hasierako ekintzetarako erabiliko den URI kointzidentzia detektatzeko modu lehenetsia." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "Eranskinentzako 1GB-eko zifratutako biltegia." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Zure premium bazkidetza bitwarden.com webguneko kutxa gotorrean ordaindu dezakezu. Orain bisitatu nahi duzu webgunea?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Premium bazkide zara!" }, "premiumCurrentMemberThanks": { "message": "Eskerrik asko Bitwarden babesteagatik." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "Dena, urtean $PRICE$gatik!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Eguneratzea eginda" }, @@ -1178,14 +1341,23 @@ "message": "Inguruneko URL-ak gorde dira." }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,20 +1371,39 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "Auto-bete orrialdea kargatzean" }, "enableAutoFillOnPageLoadDesc": { "message": "Saio-hasierako formulario bat detektatzen bada, auto-bete webgunea kargatzen denean." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Compromised or untrusted websites can exploit autofill on page load." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" }, "learnMoreAboutAutofill": { - "message": "Learn more about auto-fill" + "message": "Learn more about autofill" }, "defaultAutoFillOnPageLoad": { "message": "Saio-hasierako elementuetarako lehenetsitako auto-betetzearen konfigurazioa" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Alboko barran ireki kutxa gotorra" }, - "commandAutofillDesc": { - "message": "Uneko webgunerako erabilitako azken saio-hastea auto-bete" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Zorizko pasahitz berria sortu eta kopiatu arbelean" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolearra" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Lotuta", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Pasahitz historia" }, @@ -1533,6 +1742,10 @@ "message": "Oinarrizko domeinua", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domeinu izena", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Detekzio modua", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Lehenetsitako detekzio modua", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Txandaketa aukerak" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Ez dago erakusteko pasahitzik." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Ezabatu" }, @@ -1677,6 +1899,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" }, @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Zure pasahitz nagusi berriak ez ditu baldintzak betetzen." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Kanporatutako domeinuak" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ ez da onartutako domeinu bat", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Pasahitz babestua" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Send esteka kopiatu", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send-a sortua", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send-a editatua", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Egiaztapen emaila beharrezkoa da" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Emaila egiaztatu behar duzu funtzio hau erabiltzeko. Emaila web-eko kutxa gotorrean egiazta dezakezu." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Izen-emate automatikoa" }, @@ -2606,10 +2906,10 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { - "message": "How to auto-fill" + "message": "How to autofill" }, "autofillSelectInfoWithCommand": { "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", @@ -2627,16 +2927,25 @@ "message": "Ulertuta" }, "autofillSettings": { - "message": "Auto-fill settings" + "message": "Autofill settings" + }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Berretsi" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 288da2b9945..224fa273005 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -7,12 +7,15 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "در خانه، محل کار و هر کجای دیگر، Bitwarden به راحتی همه‌ رمزها و کلیدهای عبور، و اطلاعات حساس شما را ایمن می‌کند", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "وارد شوید یا یک حساب کاربری بسازید تا به گاوصندوق امن‌تان دسترسی یابید." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "ایجاد حساب کاربری" }, @@ -20,10 +23,7 @@ "message": "تنظیم رمز عبور قوی" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" - }, - "login": { - "message": "ورود" + "message": "ایجاد حساب خود را با تنظیم رمز عبور تکمیل کنید" }, "enterpriseSingleSignOn": { "message": "ورود به سیستم پروژه" @@ -68,6 +68,12 @@ "masterPassHint": { "message": "یادآور کلمه عبور اصلی (اختیاری)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "زبانه" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "کپی کد امنیتی" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "پر کردن خودکار" }, @@ -192,19 +213,19 @@ "message": "Continue to web app?" }, "continueToWebAppDesc": { - "message": "Explore more features of your Bitwarden account on the web app." + "message": "ویژگی‌های بیشتر حساب Bitwarden خود را در برنامه وب کاوش کنید." }, "continueToHelpCenter": { "message": "Continue to Help Center?" }, "continueToHelpCenterDesc": { - "message": "Learn more about how to use Bitwarden on the Help Center." + "message": "درباره استفاده از Bitwarden در مرکز راهنما بیشتر بیاموزید." }, "continueToBrowserExtensionStore": { - "message": "Continue to browser extension store?" + "message": "آیا میخواهید به فروشگاه افزونه مرورگر ادامه دهید?" }, "continueToBrowserExtensionStoreDesc": { - "message": "Help others find out if Bitwarden is right for them. Visit your browser's extension store and leave a rating now." + "message": "به دیگران کمک کنید تا بفهمند آیا Bitwarden برایشان مناسب است یا نه. به فروشگاه افزونه مرورگر خود بروید و نظر خود را به اشتراک بگذارید." }, "changeMasterPasswordOnWebConfirmation": { "message": "You can change your master password on the Bitwarden web app." @@ -280,6 +301,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,16 +384,56 @@ "message": "حداقل طول گذرواژه" }, "uppercase": { - "message": "حروف بزرگ (A-Z)" + "message": "حروف بزرگ (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "حروف کوچک (a-z)" + "message": "حروف کوچک (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "اعداد (‪0-9‬)" + "message": "اعداد (‪0-9‬)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "نویسه‌های ویژه (!@#$%^&*)" + "message": "نویسه‌های ویژه (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "تعداد کلمات" @@ -376,7 +455,12 @@ "message": "حداقل حرف خاص" }, "avoidAmbChar": { - "message": "از کاراکترهای مبهم اجتناب کن" + "message": "از کاراکترهای مبهم اجتناب کن", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "جستجوی گاوصندوق" @@ -556,6 +640,18 @@ "security": { "message": "امنیت" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "خطایی رخ داده است" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "حساب کاربری جدید شما ساخته شد! حالا می‌توانید وارد شوید." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "شما با موفقیت وارد شدید" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "کد تأیید مورد نیاز است." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "کد تأیید نامعتبر است" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "نشست ورود شما منقضی شده است." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "آیا مطمئنید که می‌خواهید خارج شوید؟" }, @@ -697,6 +829,10 @@ "newUri": { "message": "نشانی اینترنتی جدید" }, + "addDomain": { + "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": { "message": "مورد اضافه شد" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "درخواست افزودن ورود به سیستم" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "در صورتی که موردی در گاوصندوق شما یافت نشد، درخواست افزودن کنید." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "نمایش کارت‌ها در صفحه برگه" }, "showCardsCurrentTabDesc": { "message": "برای پر کردن خودکار آسان، موارد کارت را در صفحه برگه فهرست کن." }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" + }, "showIdentitiesCurrentTab": { "message": "نشان دادن هویت در صفحه برگه" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "بررسی مطابقت نشانی اینترنتی پیش‌فرض", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "هنگام انجام دادن کارهایی مانند پر کردن خودکار، روش پیش‌فرضی را که برای شناسایی ورود نشانی اینترنتی انجام می‌شود انتخاب کنید." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "۱ گیگابایت فضای ذخیره سازی رمزگذاری شده برای پیوست های پرونده." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "گزینه های ورود اضافی دو مرحله ای مانند YubiKey و Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "شما می‌توانید عضویت پرمیوم را از گاوصندوق وب bitwarden.com خریداری کنید. مایلید اکنون از وب‌سایت بازید کنید؟" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "شما یک عضو پرمیوم هستید!" }, "premiumCurrentMemberThanks": { "message": "برای حمایتتان از Bitwarden سپاسگزاریم." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "تمامش فقط $PRICE$ در سال!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "نوسازی کامل شد" }, @@ -1178,14 +1341,23 @@ "message": "نشانی‌های اینترنتی محیط ذخیره شد" }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "ویرایش تنظیمات مرورگر." @@ -1199,18 +1371,37 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "پر کردن خودکار هنگام بارگذاری صفحه" }, "enableAutoFillOnPageLoadDesc": { "message": "اگر یک فرم ورودی شناسایی شد، وقتی صفحه وب بارگذاری شد، به صورت خودکار پر شود." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "وب‌سایت‌های در معرض خطر یا نامعتبر می‌توانند از پر کردن خودکار در بارگذاری صفحه سوء استفاده کنند." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" + }, "learnMoreAboutAutofill": { "message": "درباره پر کردن خودکار بیشتر بدانید" }, @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "باز کردن گاوصندوق در نوار کناری" }, - "commandAutofillDesc": { - "message": "آخرین ورودی مورد استفاده برای وب سایت فعلی را به صورت خودکار پر کنید" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "یک کلمه عبور تصادفی جدید ایجاد کنید و آن را در کلیپ بورد کپی کنید" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "منطقی" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "پیوند شده", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "تاریخچه کلمه عبور" }, @@ -1533,6 +1742,10 @@ "message": "دامنه پایه", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "نام دامنه", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "تشخیص مطابقت", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "بررسی مطابقت پیش‌فرض", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "گزینه های تبدیل" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "هیچ کلمه عبوری برای فهرست کردن وجود ندارد." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "حذف" }, @@ -1677,6 +1899,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": "عمل متوقف شدن گاو‌صندوق" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "کلمه عبور اصلی جدید شما از شرایط سیاست پیروی نمی‌کند." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "لغو اشتراک" @@ -1874,6 +2119,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": "بیومتریک برپا نشده" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "زیست‌سنجی ناموفق بود" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "دامنه های مستثنی" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ دامنه معتبری نیست", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "ارسال", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "محافظت ‌شده با کلمه عبور" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "پیوند ارسال را کپی کن", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "ارسال ساخته شد", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "ارسال ذخیره شد", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "تأیید ایمیل لازم است" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "برای استفاده از این ویژگی باید ایمیل خود را تأیید کنید. می‌توانید ایمیل خود را در گاوصندوق وب تأیید کنید." }, @@ -2179,6 +2476,9 @@ "updateWeakMasterPasswordWarning": { "message": "کلمه عبور اصلی شما با یک یا چند سیاست سازمان‌تان مطابقت ندارد. برای دسترسی به گاوصندوق، باید همین حالا کلمه عبور اصلی خود را به‌روز کنید. در صورت ادامه، شما از نشست فعلی خود خارج می‌شوید و باید دوباره وارد سیستم شوید. نشست فعال در دستگاه های دیگر ممکن است تا یک ساعت همچنان فعال باقی بمانند." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "resetPasswordPolicyAutoEnroll": { "message": "ثبت نام خودکار" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "تنظیمات پر کردن خودکار" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" + }, "autofillShortcut": { "message": "میانبر صفحه کلید پر کردن خودکار" }, - "autofillShortcutNotSet": { - "message": "میانبر پر کردن خودکار تنظیم نشده است. این را در تنظیمات مرورگر تغییر دهید." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "میانبر پر کردن خودکار: $COMMAND$ است. این را در تنظیمات مرورگر تغییر دهید.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "دستگاه مورد اعتماد است" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "ورودی ضروری است." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- انتخاب --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "موارد با درخواست مجدد کلمه عبور اصلی را نمی‌توان در بارگذاری صفحه به‌صورت خودکار پر کرد. پر کردن خودکار در بارگیری صفحه خاموش شد.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "پر کردن خودکار در بارگیری صفحه برای استفاده از تنظیمات پیش‌فرض تنظیم شده است.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "برای ویرایش این فیلد، درخواست مجدد کلمه عبور اصلی را خاموش کنید", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "افزودن موردی جدید به گاوصندوق", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,8 +3486,8 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "تأیید توسط سایت آغازگر الزامی است. این ویژگی هنوز برای حساب‌های بدون کلمه عبور اصلی اجرا نشده است." }, - "logInWithPasskey": { - "message": "با کلید عبور وارد می‌شوید؟" + "logInWithPasskeyQuestion": { + "message": "Log in with passkey?" }, "passkeyAlreadyExists": { "message": "یک کلید عبور از قبل برای این برنامه وجود دارد." @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "شما هیچ ورود مشابهی برای این سایت ندارید." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "تأیید" }, @@ -3143,8 +3510,11 @@ "savePasskeyNewLogin": { "message": "کلید عبور را به عنوان ورود جدید ذخیره کن" }, - "choosePasskey": { - "message": "یک ورود برای ذخیره این کلید عبور انتخاب کنید" + "chooseCipherForPasskeySave": { + "message": "Choose a login to save this passkey to" + }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" }, "passkeyItem": { "message": "مورد کلید عبور" @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index e8aaee19aa3..927225fce52 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -7,12 +7,15 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Kotona, töissä tai reissussa, Bitwarden suojaa salasanasi, suojausavaimesi ja arkaluonteiset tietosi helposti.", + "message": "Kotona, töissä tai reissussa, Bitwarden suojaa salasanasi, pääsyavaimesi ja arkaluonteiset tietosi helposti.", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Käytä salattua holviasi kirjautumalla sisään tai luo uusi tili." }, + "inviteAccepted": { + "message": "Kutsu hyväksyttiin" + }, "createAccount": { "message": "Luo tili" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Viimeistele tilin luonti asettamalla salasana" }, - "login": { - "message": "Kirjaudu" - }, "enterpriseSingleSignOn": { "message": "Yrityksen kertakirjautuminen (SSO)" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Pääsalasanan vihje (valinnainen)" }, + "joinOrganization": { + "message": "Liity organisaatioon" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Viimeistele liittyminen organisaatioon asettamalla pääsalasana." + }, "tab": { "message": "Välilehti" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Kopioi turvakoodi" }, + "copyName": { + "message": "Kopioi nimi" + }, + "copyCompany": { + "message": "Kopioi yritys" + }, + "copySSN": { + "message": "Kopioi henkilötunnus" + }, + "copyPassportNumber": { + "message": "Kopioi passin numero" + }, + "copyLicenseNumber": { + "message": "Kopioi ajokortin numero" + }, "autoFill": { "message": "Automaattitäyttö" }, @@ -117,7 +138,7 @@ "message": "Automaattitäytä kortti" }, "autoFillIdentity": { - "message": "Automaattitäytä identiteetti" + "message": "Automaattitäytä henkilöllisyys" }, "generatePasswordCopied": { "message": "Luo salasana (leikepöydälle)" @@ -141,7 +162,7 @@ "message": "Lisää kortti" }, "addIdentityMenu": { - "message": "Lisää identiteetti" + "message": "Lisää henkilöllisyys" }, "unlockVaultMenu": { "message": "Avaa holvisi" @@ -242,7 +263,7 @@ "message": "Bitwarden Authenticator" }, "continueToAuthenticatorPageDesc": { - "message": "Bitwarden Authenticatorin avulla voit säilyttää todennusavaimet ja luoda TOTP-koodeja kaksivaiheista tunnistautumista varten. Lue lisää bitwarden.com-sivustolta." + "message": "Bitwarden Authenticatorin avulla voit säilyttää todennusavaimet ja luoda TOTP-koodeja kaksivaiheista kirjautumista varten. Lue lisää bitwarden.com-sivustolta." }, "bitwardenSecretsManager": { "message": "Bitwarden Salaisuushallinta" @@ -280,6 +301,24 @@ "editFolder": { "message": "Muokkaa kansiota" }, + "newFolder": { + "message": "Uusi kansio" + }, + "folderName": { + "message": "Kansion nimi" + }, + "folderHintText": { + "message": "Luo alikansio lisäämällä olemassa olevan kansion nimi \"/\"-merkin edelle. Esim: Some/Foorumit." + }, + "noFoldersAdded": { + "message": "Kansioita ei ole lisätty" + }, + "createFoldersToOrganize": { + "message": "Järjestä holvisi kohteita luomalla kansioita" + }, + "deleteFolderPermanently": { + "message": "Haluatko varmasti poistaa kansion pysyvästi?" + }, "deleteFolder": { "message": "Poista kansio" }, @@ -345,16 +384,56 @@ "message": "Salasanan vähimmäispituus" }, "uppercase": { - "message": "Isot kirjaimet (A-Z)" + "message": "Isot kirjaimet (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Pienet kirjaimet (a-z)" + "message": "Pienet kirjaimet (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numerot (0-9)" + "message": "Numerot (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Erikoismerkit (!@#$%^&*)" + "message": "Erikoismerkit (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Sisällytys", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Sisällytä isoja kirjaimia", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Sisällytä pieniä kirjaimia", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Sisällytä numeroita", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Sisällytä erikoismerkkejä", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Sanojen määrä" @@ -376,7 +455,12 @@ "message": "Erikoismerkkejä vähintään" }, "avoidAmbChar": { - "message": "Vältä epäselviä merkkejä" + "message": "Vältä epäselviä merkkejä", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Vältä epäselviä merkkejä", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Etsi holvista" @@ -478,7 +562,7 @@ "message": "Selaimesi ei tue helppoa leikepöydälle kopiointia. Kopioi kohde manuaalisesti." }, "verifyIdentity": { - "message": "Vahvista henkilöllisyys" + "message": "Vahvista henkilöllisyytesi" }, "yourVaultIsLocked": { "message": "Holvisi on lukittu. Jatka vahvistamalla henkilöllisyytesi." @@ -556,6 +640,18 @@ "security": { "message": "Suojaus" }, + "confirmMasterPassword": { + "message": "Vahvista pääsalasana" + }, + "masterPassword": { + "message": "Pääsalasana" + }, + "masterPassImportant": { + "message": "Pääsalasanasi palauttaminen ei ole mahdollista, jos unohdat sen!" + }, + "masterPassHintLabel": { + "message": "Pääsalasanan vihje" + }, "errorOccurred": { "message": "Tapahtui virhe" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Uusi käyttäjätilisi on luotu! Voit nyt kirjautua sisään." }, + "newAccountCreated2": { + "message": "Uusi tilisi on luotu!" + }, + "youHaveBeenLoggedIn": { + "message": "Sinut on kirjattu sisään!" + }, "youSuccessfullyLoggedIn": { "message": "Kirjautuminen onnistui" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Todennuskoodi vaaditaan." }, + "webauthnCancelOrTimeout": { + "message": "Tunnistautuminen peruttiin tai se kesti liian kauan. Yritä uudelleen." + }, "invalidVerificationCode": { "message": "Virheellinen todennuskoodi" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Skannaa todennusavaimen QR-koodi nykyiseltä verkkosivulta" }, + "totpHelperTitle": { + "message": "Tee kaksivaiheisesta kirjautumisesta saumatonta" + }, + "totpHelper": { + "message": "Bitwarden voi säilyttää ja täyttää kaksivaiheisen kirjautumisen koodit. Kopioi ja liitä todennusavain tähän kenttään." + }, + "totpHelperWithCapture": { + "message": "Bitwarden voi säilyttää ja täyttää kaksivaiheisen kirjautumisen koodit. Kamerakuvakkeella voit kaapata todennusavaimen avoimen sivun QR-koodista automaattisesti, tai voit kopioida ja liittää sen tähän kenttään manuaalisesti." + }, + "learnMoreAboutAuthenticators": { + "message": "Lue lisää todentajista" + }, "copyTOTP": { "message": "Kopioi todennusavain (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Kirjautumisistuntosi on erääntynyt." }, + "logIn": { + "message": "Kirjaudu" + }, + "restartRegistration": { + "message": "Aloita rekisteröityminen alusta" + }, + "expiredLink": { + "message": "Vanhentunut linkki" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Aloita rekisteröityminen alusta tai yritä kirjautua sisään uudelleen." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Sinulla saattaa jo olla tili" + }, "logOutConfirmation": { "message": "Haluatko varmasti kirjautua ulos?" }, @@ -655,7 +787,7 @@ "message": "Kansio lisätty" }, "twoStepLoginConfirmation": { - "message": "Kaksivaiheinen kirjautuminen parantaa tilisi suojausta vaatimalla kirjautumisen vahvistuksen salasanan lisäksi todennuslaitteen, ‑sovelluksen, tekstiviestin, puhelun tai sähköpostin avulla. Voit ottaa kaksivaiheisen kirjautumisen käyttöön bitwarden.com‑verkkoholvissa. Haluatko avata sen nyt?" + "message": "Kaksivaiheinen kirjautuminen parantaa tilisi suojausta vaatimalla kirjautumisen vahvistuksen salasanan lisäksi suojausavaimen, ‑sovelluksen, tekstiviestin, puhelun tai sähköpostin avulla. Voit ottaa kaksivaiheisen kirjautumisen käyttöön bitwarden.com‑verkkoholvissa. Haluatko avata sen nyt?" }, "editedFolder": { "message": "Kansio tallennettiin" @@ -697,6 +829,10 @@ "newUri": { "message": "Uusi URI" }, + "addDomain": { + "message": "Lisää verkkotunnus", + "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": { "message": "Kohde lisättiin" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Kysy lisätäänkö kirjautumistieto" }, + "vaultSaveOptionsTitle": { + "message": "Holvitallennuksen valinnat" + }, "addLoginNotificationDesc": { "message": "Kysy lisätäänkö uusi kohde, jos holvissa ei vielä ole sopivaa kohdetta." }, "addLoginNotificationDescAlt": { "message": "Ehdota kohteen tallennusta, jos holvistasi ei vielä löydy vastaavaa kohdetta. Koskee kaikkia kirjautuneita tilejä." }, + "showCardsInVaultView": { + "message": "Näytä kortit automaattitäytön ehdotuksina Holvi-näkymässä" + }, "showCardsCurrentTab": { "message": "Näytä kortit välilehtiosiossa" }, "showCardsCurrentTabDesc": { "message": "Kortit näytetään laajennuksen välilehtisivulla niiden automaattisen täytön helpottamiseksi." }, + "showIdentitiesInVaultView": { + "message": "Näytä henkilöllisyydet automaattitäytön ehdotuksina Holvi-näkymässä" + }, "showIdentitiesCurrentTab": { "message": "Näytä henkilöllisyydet välilehtiosiossa" }, @@ -779,10 +924,10 @@ "message": "Tarjoa kirjautumistiedon salasanan päivitystä, kun verkkosivustolla havaitaan uusi salasana. Koskee kaikkia kirjautuneita tilejä." }, "enableUsePasskeys": { - "message": "Tarjoa suojausvainten tallennusta ja käyttöä" + "message": "Tarjoa pääsyavainten tallennusta ja käyttöä" }, "usePasskeysDesc": { - "message": "Tarjoa tallennusta uusille suojausavaimille tai kirjautumista holvissasi olevilla salausavaimilla. Koskee kaikkia kirjautuneita tilejä." + "message": "Tarjoa uusien pääsyavainten tallennusta sekä kirjautumista holvissasi olevilla pääsyavaimilla. Koskee kaikkia kirjautuneita tilejä." }, "notificationChangeDesc": { "message": "Haluatko päivittää salasanan Bitwardeniin?" @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "URI:n oletuarvoinen tunnistustapa", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Valitse kirjautumistietojen URI:en oletusarvoinen tunnistustapa suoritettaessa automaattisen täytön kaltaisia toimintoja." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 Gt salattua tallennustilaa tiedostoliitteille." }, + "premiumSignUpEmergency": { + "message": "Varmuuskäyttö" + }, "premiumSignUpTwoStepOptions": { "message": "Omisteiset kaksivaiheisen kirjautumisen vaihtoehdot, kuten YubiKey ja Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Voit ostaa Premium-jäsenyyden bitwarden.com-verkkoholvista. Haluatko avata sivuston nyt?" }, + "premiumPurchaseAlertV2": { + "message": "Voit ostaa Premiumin tiliasetuksistasi Bitwardenin verkkosovelluksen kautta." + }, "premiumCurrentMember": { "message": "Olet Premium-jäsen!" }, "premiumCurrentMemberThanks": { "message": "Kiitos kun tuet Bitwardenia." }, + "premiumFeatures": { + "message": "Päivitä premium-tilaukseen ja saat:" + }, "premiumPrice": { "message": "Kaikki tämä vain $PRICE$/vuosi!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "Kaikki tämä vain $PRICE$/vuosi!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Päivitys valmistui" }, @@ -1028,7 +1191,7 @@ "message": "Kopioi TOTP-koodi automaattisesti" }, "disableAutoTotpCopyDesc": { - "message": "Jos kirjautumistieto sisältää kaksivaiheisen todennuksen avaimen, kopioidaan TOTP-todennuskoodi leikepöydälle kohteen automaattisen täytön yhteydessä." + "message": "Jos kirjautumistieto sisältää kaksivaiheisen kirjautumisen todennusavaimen, kopioidaan TOTP-koodi leikepöydälle kohteen automaattisen täytön yhteydessä." }, "enableAutoBiometricsPrompt": { "message": "Pyydä Biometristä todennusta käynnistettäessä" @@ -1040,10 +1203,10 @@ "message": "Tämä ominaisuus edellyttää Premium-jäsenyyttä." }, "enterVerificationCodeApp": { - "message": "Syötä 6-numeroinen todennuskoodi todennussovelluksestasi." + "message": "Syötä todennussovelluksesi näyttämä kuusinumeroinen todennuskoodi." }, "enterVerificationCodeEmail": { - "message": "Syötä 6-numeroinen todennuskoodi, joka lähetettiin sähköpostitse osoitteeseen $EMAIL$.", + "message": "Syötä osoitteeseen $EMAIL$ lähetetty kuusinumeroinen todennuskoodi.", "placeholders": { "email": { "content": "$1", @@ -1067,16 +1230,16 @@ "message": "Lähetä todennuskoodi sähköpostitse uudelleen" }, "useAnotherTwoStepMethod": { - "message": "Käytä toista kaksivaiheisen kirjautumisen todentajaa" + "message": "Käytä vaihtoehtoista todennustapaa" }, "insertYubiKey": { "message": "Kytke YubiKey-todennuslaitteesi tietokoneen USB-porttiin ja paina sen painiketta." }, "insertU2f": { - "message": "Kytke todennuslaitteesi tietokoneen USB-porttiin. Jos laitteessa on painike, paina sitä." + "message": "Kytke suojausavaimesi tietokoneen USB-porttiin. Jos laitteessa on painike, paina sitä." }, "webAuthnNewTab": { - "message": "Aloittaaksesi kaksivaiheisen WebAuthn-todennuksen, seuraa alla olevasta painikkeesta uuteen välilehteen avautuvia ohjeita." + "message": "Aloittaaksesi kaksivaiheisen WebAuthn-tunnistautumisen, seuraa alla olevasta painikkeesta uuteen välilehteen avautuvia ohjeita." }, "webAuthnNewTabOpen": { "message": "Avaa uusi välilehti" @@ -1110,7 +1273,7 @@ "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP -todennuslaite" + "message": "Yubico OTP -suojausavain" }, "yubiKeyDesc": { "message": "Käytä YubiKey-todennuslaitetta tilisi avaukseen. Toimii YubiKey 4, 4 Nano, 4C sekä NEO -laitteiden kanssa." @@ -1120,14 +1283,14 @@ "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { - "message": "Vahvista organisaatiollesi Duo Securityn avulla käyttäen Duo Mobile ‑sovellusta, tekstiviestiä, puhelua tai U2F-todennuslaitetta.", + "message": "Vahvista organisaatiollesi Duo Securityn avulla käyttäen Duo Mobile ‑sovellusta, tekstiviestiä, puhelua tai U2F-suojausavainta.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, "webAuthnDesc": { - "message": "Käytä mitä tahansa WebAuthn‑yhteensopivaa todennuslaitetta päästäksesi käsiksi tiliisi." + "message": "Käytä mitä tahansa WebAuthn‑yhteensopivaa suojausavainta päästäksesi tilillesi." }, "emailTitle": { "message": "Sähköposti" @@ -1179,13 +1342,22 @@ }, "showAutoFillMenuOnFormFields": { "message": "Näytä automaattitäytön valikko lomakekentissä", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Automaattitäytön ehdotukset" + }, + "showInlineMenuLabel": { + "message": "Näytä automaattitäytön ehdotukset lomakekentissä" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Näytä ehdotukset kun kuvaketta painetaan" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Koskee kaikkia kirjautuneita tilejä." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Poista selaimesi sisäänrakennettu salasanahallinta käytöstä sen asetuksista ristiriitojen välttämiseksi." + "message": "Poista käytöstä selaimesi asetuksista sen sisäänrakennettu salasanahallinta ristiriitojen välttämiseksi." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Muokkaa selaimen asetuksia" @@ -1202,14 +1374,33 @@ "message": "Kun automaattitäytön kuvaketta painetaan", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Automaattitäyttö sivun avautuessa" + }, "enableAutoFillOnPageLoad": { "message": "Automaattitäyttö sivun avautuessa" }, "enableAutoFillOnPageLoadDesc": { "message": "Automaattinen täyttö suoritetaan sivun avautuessa, jos sivulla havaitaan kirjautumislomake." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Varoitus:$CLOSETAG$ Vaarantuneet tai epäluotettavat sivustot voivat hyväksikäyttää sivun avautuessa suoritettavaa automaattista täyttöä.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { - "message": "Vaarantuneet tai epäluotettavat sivustot voivat väärinkäyttää sivun avautuessa suoritettavaa automaattista täyttöä." + "message": "Vaarantuneet tai epäluotettavat sivustot voivat hyväksikäyttää sivun avautuessa suoritettavaa automaattista täyttöä." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Lisätietoja riskeistä" }, "learnMoreAboutAutofill": { "message": "Lisätietoja automaattitäytöstä" @@ -1218,7 +1409,7 @@ "message": "Kirjautumistietojen automaattitäytön oletusasetus" }, "defaultAutoFillOnPageLoadDesc": { - "message": "Automaattinen täyttö on mahdollista ottaa käyttöön tai poistaa käytöstä kirjautumistietokohtaisesti kirjautumistetoa muokkaamalla." + "message": "Automaattinen täyttö voidaan ottaa käyttöön tai poistaa käytöstä kohdekohtaisesti muokkaamalla yksittäisiä kirjautumistietoja." }, "itemAutoFillOnPageLoad": { "message": "Automaattitäyttö sivun avautuessa (jos määritetty asetuksista)" @@ -1227,10 +1418,10 @@ "message": "Käytä oletusasetusta" }, "autoFillOnPageLoadYes": { - "message": "Automaattitäyttö sivun avautuessa" + "message": "Automaattitäytä sivun avautuessa" }, "autoFillOnPageLoadNo": { - "message": "Ei automaattitäyttöä sivun avautuessa" + "message": "Älä automaattitäytä sivun avautuessa" }, "commandOpenPopup": { "message": "Avaa holvin ponnahdusikkuna" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Avaa holvi sivupalkissa" }, - "commandAutofillDesc": { - "message": "Täytä edellinen nykyisellä sivustolla käytetty kirjautumistieto automaattisesti." + "commandAutofillLoginDesc": { + "message": "Automaattitäytä tällä sivustolla viimeksi käytetty kirjautumistieto." + }, + "commandAutofillCardDesc": { + "message": "Automaattitäytä tällä sivustolla viimeksi käytetty kortti." + }, + "commandAutofillIdentityDesc": { + "message": "Automaattitäytä tällä sivustolla viimeksi käytetty henkilöllisyys." }, "commandGeneratePasswordDesc": { "message": "Luo uusi satunnainen salasana ja kopioi se leikepöydälle." @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Totuusarvo" }, + "cfTypeCheckbox": { + "message": "Valintaruutu" + }, "cfTypeLinked": { "message": "Linkitetty", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1463,7 +1663,16 @@ } }, "editItemHeader": { - "message": "Muokkaa $TYPE$", + "message": "Muokkaa: $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, + "viewItemHeader": { + "message": "Näytä $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1533,6 +1742,10 @@ "message": "Pääverkkotunnus", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Juuriverkkotunnus (suositus)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Verkkotunnus", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Tunnistustapa", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Oletusarvoinen tunnistustapa", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Näytä tai piilota asetukset" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Ei näytettäviä salasanoja." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Poista" }, @@ -1677,6 +1899,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": "Turvallinen salasana luotiin! Muista vaihtaa se myös verkkosivuston tiliasetuksiin." + }, + "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": "luodaksesi vahvan ainutlaatuisen salasanan", + "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" }, @@ -1710,13 +1955,13 @@ "message": "Onko sinulla jo tili?" }, "vaultTimeoutLogOutConfirmation": { - "message": "Uloskirjautuminen estää pääsyn holviisi ja vaatii ajan umpeuduttua todennuksen Internet-yhteyden välityksellä. Haluatko varmasti käyttää asetusta?" + "message": "Uloskirjautuminen estää pääsyn holviisi ja vaatii ajan umpeuduttua tunnistautumisen Internet-yhteyden välityksellä. Haluatko varmasti käyttää asetusta?" }, "vaultTimeoutLogOutConfirmationTitle": { "message": "Aikakatkaisutoiminnon vahvistus" }, "autoFillAndSave": { - "message": "Automaattitäytä ja tallenna" + "message": "Täytä automaattisesti ja tallenna" }, "fillAndSave": { "message": "Täytä ja tallenna" @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Uusi pääsalasanasi ei täytä käytännön määrittämiä vaatimuksia." }, - "receiveMarketingEmails": { - "message": "Vastaanota Bitwardenilta uutiskirjeitä julkaisuista, tukiresursseista ja tutkimusmahdollisuuksista." + "receiveMarketingEmailsV2": { + "message": "Vastaanota Bitwardenilta postilaatikkoosi vinkkejä, uutisia ja tutkimusmahdollisuuksia." }, "unsubscribe": { "message": "Lopeta tilaus" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Poista käyttäjän lukitus työpöytäsovelluksesta ja yritä uudelleen." }, + "biometricsNotAvailableTitle": { + "message": "Biometrinen avaus ei ole käytettävissä" + }, + "biometricsNotAvailableDesc": { + "message": "Biometrinen avaus ei tällä hetkellä ole käytettävissä. Yritä myöhemmin uudelleen." + }, "biometricsFailedTitle": { "message": "Biometria epäonnistui" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "Organisaatiokäytäntö estää kohteiden tuonnin yksityiseen holviisi." }, + "domainsTitle": { + "message": "Verkkotunnukset", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Ohitettavat verkkotunnukset" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden ei pyydä kirjautumistietojen tallennusta näillä verkkotunnuksilla. Koskee kaikkia kirjautuneita tilejä. Ota muutokset käyttöön päivittämällä sivu." }, + "websiteItemLabel": { + "message": "Verkkotunnus $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ ei ole kelvollinen verkkotunnus", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Rajoitettujen verkkotunnusten muutokset tallennettiin" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Salasanasuojattu" }, + "copyLink": { + "message": "Kopioi linkki" + }, "copySendLink": { "message": "Kopioi Send-linkki", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send luotiin", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Sendin luonti onnistui!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "Tämän linkin välityksellä Send on kenen tahansa avattavissa seuraavien $DAYS$ päivän ajan.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send-linkki kopioitiin", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send tallennettiin", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Sähköpostiosoite on vahvistettava" }, + "emailVerifiedV2": { + "message": "Sähköpostiosoite on vahvistettu" + }, "emailVerificationRequiredDesc": { "message": "Sinun on vahvistettava sähköpostiosoitteesi käyttääksesi ominaisuutta. Voit vahvistaa osoitteesi verkkoholvissa." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automaattinen liitos" }, @@ -2561,7 +2861,7 @@ "message": "Tunnistelauseke" }, "fingerprintMatchInfo": { - "message": "Varmista, että holvisi on avattu ja tunnistelauseke täsmää toisella laitteella." + "message": "Varmista, että vahvistavan laitteen holvi on avattu ja että se näyttää saman tunnistelausekkeen." }, "resendNotification": { "message": "Lähetä ilmoitus uudelleen" @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Automaattitäytön asetukset" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Automaattitäytön pikanäppäin" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Muuta pikanäppäintä" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Hallitse pikanäppäimiä" + }, "autofillShortcut": { "message": "Automaattitäytön pikanäppäin" }, - "autofillShortcutNotSet": { - "message": "Automaattisen täytön pikanäppäintä ei ole määritetty. Määritä se selaimen asetuksista." + "autofillLoginShortcutNotSet": { + "message": "Automaattitäytön pikanäppäintä ei ole määritetty. Määritä se selaimen asetuksista." }, - "autofillShortcutText": { - "message": "Automaattisen täytön pikanäppäin on $COMMAND$. Vaihda se selaimen asetuksista.", + "autofillLoginShortcutText": { + "message": "Kirjautumistiedon automaatttitäytön pikanäppäin on $COMMAND$. Hallitse pikanäppäimiä selaimen asetuksista.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Laitteeseen luotettu" }, + "sendsNoItemsTitle": { + "message": "Aktiivisia Sendejä ei ole", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Sendillä voit jakaa salattuja tietoja turvallisesti kenelle tahansa.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Syöte vaaditaan." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "Yksi kenttä vaatii huomiotasi." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ kenttää vaatii huomiotasi.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Valitse --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Kohteille, joille on määritetty pääsalasanan uudelleenkysely, ei voida suorittaa automaattista täyttöä sivun avautuessa. Automaattitäyttö sivun avautuessa poistettiin käytöstä. avautuessa suoritettavan", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "Automaattitäyttö sivun avautuessa käyttää oletusasetusta.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Poista pääsalasanan uudelleenkysely käytöstä muokataksesi kenttää", @@ -2911,10 +3240,18 @@ "message": "Näytä sopivat kirjautumistiedot avaamalla tilisi lukitus", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Näytä automaattitäytön ehdotukset avaamalla tilisi lukitus", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Avaa tili", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Avaa tilisi lukitus. Avautuu uudessa ikkunassa.", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Täytä kirjautumistiedot kohteesta", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Lisää holviin uusi kohde", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "Uusi kirjautumistieto", + "description": "Button text to display within inline menu when there are no matching items on a login field" + }, + "addNewLoginItemAria": { + "message": "Lisää holviin uusi kirjautumistieto. Avautuu uudessa ikkunassa.", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "Uusi kortti", + "description": "Button text to display within inline menu when there are no matching items on a credit card field" + }, + "addNewCardItemAria": { + "message": "Lisää holviin uusi korttitieto. Avautuu uudessa ikkunassa.", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "Uusi henkilöllisyys", + "description": "Button text to display within inline menu when there are no matching items on an identity field" + }, + "addNewIdentityItemAria": { + "message": "Lisää holviin uusi henkilöllisyys. Avautuu uudessa ikkunassa.", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Bitwardenin automaattisen täytön valikko on käytettävissä. Valitse painamalla alas-nuolinäppäintä.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Virhe yhdistettäessä Duo-palveluun. Käytä vaihtoehtoista todennustapaa tai ole yhteydessä Duon asiakaspalveluun saadaksesi apua." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Avaa Duo ja viimeistele kirjautuminen seuraamalla ohjeita." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Tiedoston salasana on virheellinen. Käytä vientitiedoston luonnin yhteydessä syötettyä salasanaa." }, - "importDestination": { - "message": "Tuontikohde" + "destination": { + "message": "Kohde" }, "learnAboutImportOptions": { "message": "Lue lisää tuontivaihtoehdoista" @@ -3111,55 +3475,61 @@ "message": "Holvin tiedot on viety" }, "typePasskey": { - "message": "Suojausavain" + "message": "Pääsyavain" }, "passkeyNotCopied": { - "message": "Suojausavainta ei kopioida" + "message": "Pääsyavainta ei kopioida" }, "passkeyNotCopiedAlert": { - "message": "Suojausavain ei kopioidu kloonattuun kohteeseen. Haluatko jatkaa kloonausta?" + "message": "Pääsyavain ei kopioidu kloonattuun kohteeseen. Haluatko jatkaa kloonausta?" }, "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { - "message": "Käynnistävä sivusto edellyttää todennusta. Ominaisuutta ei ole vielä toteutettu tileille, joilla ei ole pääsalasanaa." + "message": "Käynnistänyt sivusto edellyttää vahvistusta. Ominaisuutta ei ole vielä toteutettu tileille, joilla ei ole pääsalasanaa." }, - "logInWithPasskey": { - "message": "Kirjaudutaanko suojausavaimella?" + "logInWithPasskeyQuestion": { + "message": "Kirjaudutaanko pääsyavaimella?" }, "passkeyAlreadyExists": { - "message": "Tälle sovellukselle on jo tallennettu suojausavain." + "message": "Tälle sovellukselle on jo tallennettu pääsyavain." }, "noPasskeysFoundForThisApplication": { - "message": "Tälle sovellukselle ei löytynyt suojausavaimia." + "message": "Tälle sovellukselle ei löytynyt pääsyavaimia." }, "noMatchingPasskeyLogin": { "message": "Holvissasi ei ole tälle sivustolle sopivaa kirjautumistietoa." }, + "noMatchingLoginsForSite": { + "message": "Ei tälle sivustolle sopivia kirjautumistietoja" + }, "confirm": { "message": "Vahvista" }, "savePasskey": { - "message": "Tallenna suojausavain" + "message": "Tallenna pääsyavain" }, "savePasskeyNewLogin": { - "message": "Tallenna suojausavain uuteen kirjautumistietoon" + "message": "Tallenna pääsyavain uuteen kirjautumistietoon" }, - "choosePasskey": { - "message": "Valitse kirjautumistieto, johon suojausavain tallennetaan" + "chooseCipherForPasskeySave": { + "message": "Valitse kirjautumistieto, johon pääsyavain tallennetaan" + }, + "chooseCipherForPasskeyAuth": { + "message": "Valitse pääsyavain, jolla kirjaudutaan" }, "passkeyItem": { - "message": "Suojausavainkohde" + "message": "Pääsyavainkohde" }, "overwritePasskey": { - "message": "Korvataanko suojausavain?" + "message": "Korvataanko pääsyavain?" }, "overwritePasskeyAlert": { - "message": "Kohde sisältää jo suojausavaimen. Haluatko varmasti korvata nykyisen suojausavaimen?" + "message": "Kohde sisältää jo pääsyavaimen. Haluatko varmasti korvata nykyisen avaimen?" }, "featureNotSupported": { "message": "Ominaisuutta ei vielä tueta" }, "yourPasskeyIsLocked": { - "message": "Salausavaimen käyttö edellyttää todennusta. Jatka vahvistamalla henkilöllisyytesi." + "message": "Pääsyavaimen käyttö edellyttää tunnistautumista. Jatka vahvistamalla henkilöllisyytesi." }, "multifactorAuthenticationCancelled": { "message": "Monivaiheinen todennus peruttiin" @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,6 +3659,30 @@ "message": "Yleiset muodot", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Avataanko selaimen asetukset?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Avataanko tukikeskus?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" + }, + "confirmContinueToHelpCenterPasswordManagementContent": { + "message": "Muuta selaimesi automaattisen täytön ja salasanojen hallinnan asetuksia.", + "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": "Voit tarkastella ja asettaa laajennusten pikavalintoja selaimesi asetuksissa.", + "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": "Muuta selaimesi automaattisen täytön ja salasanojen hallinnan asetuksia.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" + }, + "confirmContinueToBrowserKeyboardShortcutSettingsContent": { + "message": "Voit tarkastella ja asettaa laajennusten pikavalintoja selaimesi asetuksissa.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Määritetäänkö Bitwarden oletusarvoiseksi salasanahallinnaksi?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" @@ -3317,10 +3711,18 @@ "message": "Käyttäjätiedot tallennettiin!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Salasana tallennettiin!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Käyttäjätiedot päivitettiin!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Salasana vaihdettiin!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Virhe tallennettaessa käyttäjätietoja. Näet isätietoja hallinnasta.", "description": "Notification message for when saving credentials has failed." @@ -3329,30 +3731,16 @@ "message": "Onnistui" }, "removePasskey": { - "message": "Poista suojausavain" + "message": "Poista pääsyavain" }, "passkeyRemoved": { - "message": "Suojausavain poistettiin" - }, - "unassignedItemsBannerNotice": { - "message": "Huomioi: Määrittämättömät organisaatiokohteet eivät enää näy \"Kaikki holvit\" -näkymässä, vaan ne ovat käytettävissä vain Hallintapaneelista." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Huomioi: Alkaen 16. toukokuuta 2024, määrittämättömät organisaatiokohteet eivät enää näy \"Kaikki holvit\" -näkymässä, vaan ne ovat käytettävissä vain Hallintapaneelista." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Määritä nämä kohteet kokoelmaan", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ", jotta ne näkyvät.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + "message": "Pääsyavain poistettiin" }, "autofillSuggestions": { - "message": "Automaattitäytä ehdotukset" + "message": "Automaattitäytön ehdotukset" }, "autofillSuggestionsTip": { - "message": "Tallenna sivustolle kirjautumiskohde automaattista täyttöä varten" + "message": "Tallenna tälle sivustolle automaattisesti täytettävä kirjautumistieto." }, "yourVaultIsEmpty": { "message": "Holvisi on tyhjä" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Automaattitäyttö - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Automaattitäytä - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "Ei kopioitavia arvoja" }, - "assignCollections": { - "message": "Määritä kokoelmat" + "assignToCollections": { + "message": "Määritä kokoelmiin" }, "copyEmail": { "message": "Kopioi sähköpostiosoite" @@ -3493,13 +3881,13 @@ "message": "Kansiottomat kohteet" }, "itemDetails": { - "message": "Item details" + "message": "Kohteen tiedot" }, "itemName": { - "message": "Item name" + "message": "Kohteen nimi" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Et voi poistaa kokoelmia Vain katselu -oikeuksilla: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3899,33 @@ "message": "Organisaatio on poistettu käytöstä" }, "owner": { - "message": "Owner" + "message": "Omistaja" }, "selfOwnershipLabel": { - "message": "You", + "message": "Sinä", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Käytöstä poistettujen organisaatioiden kohteet eivät ole käytettävissä. Ole yhteydessä organisaation omistajaan saadaksesi apua." }, + "additionalInformation": { + "message": "Lisätiedot" + }, + "itemHistory": { + "message": "Kohteen historia" + }, + "lastEdited": { + "message": "Viimeksi muokattu" + }, + "ownerYou": { + "message": "Omistaja: Sinä" + }, + "linked": { + "message": "Linkitetty" + }, + "copySuccessful": { + "message": "Kopiointi onnistui" + }, "upload": { "message": "Lähetä" }, @@ -3548,7 +3954,7 @@ } }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Haluatko varmasti poistaa tämän liitteen pysyvästi?" + "message": "Haluatko varmasti poistaa liitteen pysyvästi?" }, "premium": { "message": "Premium" @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Suodattimet" + }, + "personalDetails": { + "message": "Henkilökohtaiset tiedot" + }, + "identification": { + "message": "Tunnistautuminen" + }, + "contactInfo": { + "message": "Yhteystiedot" + }, + "downloadAttachment": { + "message": "Lataa - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "kortin numero päättyy", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Kirjautumistiedot" + }, + "authenticatorKey": { + "message": "Todennusavain" + }, + "autofillOptions": { + "message": "Automaattitäytön asetukset" + }, + "websiteUri": { + "message": "Verkkosivusto (URI)" + }, + "websiteUriCount": { + "message": "Verkkosivusto (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": "Verkkosivusto lisättiin" + }, + "addWebsite": { + "message": "Lisää verkkosivusto" + }, + "deleteWebsite": { + "message": "Poista verkkosivusto" + }, + "defaultLabel": { + "message": "Oletus ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Näytä vastaavuuden tunnistus $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Piilota vastaavuuden tunnistus $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Automaattitäytetäänkö sivun avautuessa?" + }, + "cardExpiredTitle": { + "message": "Kortti on vanhentunut" + }, + "cardExpiredMessage": { + "message": "Jos olet uusinut sen, päivitä kortin tiedot" + }, + "cardDetails": { + "message": "Kortin tiedot" + }, + "cardBrandDetails": { + "message": "$BRAND$-tiedot", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Käytä animaatioita" + }, + "addAccount": { + "message": "Lisää tili" + }, + "loading": { + "message": "Ladataan" + }, + "data": { + "message": "Tiedot" + }, + "passkeys": { + "message": "Pääsyavaimet", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Salasanat", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Kirjaudu pääsyavaimella", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, + "assign": { + "message": "Määritä" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Vain näiden kokoelmien käyttöoikeuden omaavat organisaation jäsenet voivat nähdä kohteen." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Vain näiden kokoelmien käyttöoikeuden omaavat organisaation jäsenet voivat nähdä kohteet." + }, + "bulkCollectionAssignmentWarning": { + "message": "Olet valinnut $TOTAL_COUNT$ kohdetta. Näistä $READONLY_COUNT$ et voi muuttaa, koska käyttöoikeutesi eivät salli muokkausta.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Lisää kenttä" + }, + "add": { + "message": "Lisää" + }, + "fieldType": { + "message": "Kentän tyyppi" + }, + "fieldLabel": { + "message": "Kentän nimi" + }, + "textHelpText": { + "message": "Käytä tekstikenttiä esimerkiksi turvakysymysten kaltaisille tiedoille." + }, + "hiddenHelpText": { + "message": "Käytä piilotettuja kenttiä esimerkiksi salasanojen kaltaisille arkaluonteisille tiedoille." + }, + "checkBoxHelpText": { + "message": "Käytä valintaruutuja esimerkiksi sähköpostiosoitteen muistamisen kaltaisten valintaruutujen automaattiseen merkintään." + }, + "linkedHelpText": { + "message": "Käytä linkitettyjä kenttiä kohdatessasi sivustokohtaisia automaattitäytön ongelmia." + }, + "linkedLabelHelpText": { + "message": "Syötä kentän HTML-koodista löytyvä id-, name-, aria-label- tai placeholder-arvo." + }, + "editField": { + "message": "Muokkaa kenttää" + }, + "editFieldLabel": { + "message": "Muokkaa kohdetta $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Poista $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ lisättiin", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Siirrä $LABEL$. Liikuta kohdetta ylös- tai alaspäin nuolinäppäimillä.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ siirrettiin ylemmäs, sijainti: $INDEX$/$LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Valitse määritettävät kokoelmat" + }, + "personalItemTransferWarningSingular": { + "message": "1 kohde siirretään pysyvästi valitulle organisaatiolle, jonka jälkeen et enää omista sitä." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ kohdetta siirretään pysyvästi valitulle organisaatiolle, jonka jälkeen et enää omista niitä.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 kohde siirretään pysyvästi organisaatiolle $ORG$, jonka jälkeen et enää omista sitä.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ kohdetta siirretään pysyvästi organisaatiolle $ORG$, jonka jälkeen et enää omista niitä.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Kokoelmat määritettiin" + }, + "nothingSelected": { + "message": "Et ole valinnut mitään." + }, + "movedItemsToOrg": { + "message": "Valitut kohteet siirrettiin organisaatiolle $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Kohteet siirrettiin organisaatiolle $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Kohde siirrettiin organisaatiolle $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ siirrettiin alemmas, sijainti: $INDEX$/$LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Kohteen sijainti" + }, + "fileSends": { + "message": "Tiedosto-Sendit" + }, + "textSends": { + "message": "Teksti-Sendit" + }, + "bitwardenNewLook": { + "message": "Bitwardenilla on uusi ulkoasu!" + }, + "bitwardenNewLookDesc": { + "message": "Automaattitäyttäminen ja hakujen tekeminen Holvi-välilehdeltä on entistä helpompaa ja intuitiivisempaa. Kokeile nyt!" + }, + "accountActions": { + "message": "Tilitoiminnot" + }, + "showNumberOfAutofillSuggestions": { + "message": "Näytä automaattitäytön kirjautumistietoehdotusten määrä laajennuksen kuvakkeessa" + }, + "systemDefault": { + "message": "Järjestelmän oletus" + }, + "enterprisePolicyRequirementsApplied": { + "message": "Yrityskäytännön vaatimuksia on sovellettu tähän asetukseen" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Roskakorin kohteet" + }, + "noItemsInTrash": { + "message": "Roskakorissa ei ole kohteita" + }, + "noItemsInTrashDesc": { + "message": "Poistamasi kohteet näkyvät täällä ja poistetaan pysyvästi 30 päivän kuluttua." + }, + "trashWarning": { + "message": "Roskakorissa yli 30 päivää olleet kohteet poistetaan automaattisesti" + }, + "restore": { + "message": "Palauta" + }, + "deleteForever": { + "message": "Poista pysyvästi" + }, + "noEditPermissions": { + "message": "Sinulla ei ole oikeutta muokata tätä kohdetta" } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 813bd25014b..6a8288365e0 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Maglog-in o gumawa ng bagong account para ma-access ang iyong ligtas na kahadeyero." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "Gumawa ng Account" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "Mag-login" - }, "enterpriseSingleSignOn": { "message": "Enterprise Single Sign-On sa Filipino ay Isang Sign-On na Enterprise" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Mungkahi sa Master Password (opsyonal)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "Tab" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Kopyahin ang code ng seguridad" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "Auto-fill sa Filipino ay Awtomatikong Pagpuno" }, @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "Minimum password length" }, "uppercase": { - "message": "Uppercase (A-Z)" + "message": "Uppercase (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Lowercase (a-t) sa Filipino ay mababang-letra (a-t)" + "message": "Lowercase (a-t) sa Filipino ay mababang-letra (a-t)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numbers (0-9)" + "message": "Numbers (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Mga espesyal na character (!@#$%^&*) sa Filipino ay tinatawag na mga simbolong pambihira" + "message": "Mga espesyal na character (!@#$%^&*) sa Filipino ay tinatawag na mga simbolong pambihira", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Ang bilang ng mga salita\n\nNumero ng mga salita" @@ -376,7 +455,12 @@ "message": "Inakamababang espesyal" }, "avoidAmbChar": { - "message": "Iwasan ang mga hindi malinaw na character" + "message": "Iwasan ang mga hindi malinaw na character", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Hanapin ang vault" @@ -556,6 +640,18 @@ "security": { "message": "Kaligtasan" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Nagkaroon ng error" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Nalikha na ang iyong bagong account! Maaari ka nang mag-log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Kinakailangan ang verification code." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Maling verification code" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Nag-expire na ang iyong session sa login." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Sigurado ka bang gusto mong mag-log out?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Bagong URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "Ang item ay idinagdag" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Tanungin na magdagdag ng login" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "Tanungin na magdagdag ng isang item kung wala itong nakita sa iyong vault." }, "addLoginNotificationDescAlt": { "message": "Hilingin na magdagdag ng isang item kung ang isa ay hindi mahanap sa iyong vault. Nalalapat sa lahat ng naka-log in na account." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Ipakita ang mga card sa Tab page" }, "showCardsCurrentTabDesc": { "message": "Itala ang mga item ng card sa Tab page para sa madaling auto-fill." }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" + }, "showIdentitiesCurrentTab": { "message": "Ipakita ang mga pagkatao sa Tab page" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Default na pagtukoy ng tugma ng URI", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Pumili ng default na paraan ng paghanda ng URI match detection para sa mga login kapag ginagawa ang mga aksyon tulad ng auto-fill." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage para sa mga file attachment." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Pagmamay-ari na dalawang hakbang na opsyon sa pag-log in gaya ng YubiKey at Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Maaari kang mamili ng membership sa Premium sa website ng bitwarden.com. Gusto mo bang bisitahin ang website ngayon?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Ikaw ay isang Premium na miyembro!" }, "premiumCurrentMemberThanks": { "message": "Salamat sa pagsuporta sa Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "Lahat para lamang sa $PRICE$ /taon!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "I-refresh ang lahat" }, @@ -1178,14 +1341,23 @@ "message": "Nai-save ang mga URL ng Kapaligiran" }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,18 +1371,37 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "Awtomatikong punan sa pagkarga ng pahina" }, "enableAutoFillOnPageLoadDesc": { "message": "Kung natukoy ang isang form sa pag login, awtomatikong punan kapag naglo load ang web page." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Ang mga nakompromiso o hindi pinagkakatiwalaang mga website ay maaaring samantalahin ang awtomatikong pagpuno sa pag load ng pahina." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" + }, "learnMoreAboutAutofill": { "message": "Matuto nang higit pa tungkol sa auto fill" }, @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Buksan ang vault sa sidebar" }, - "commandAutofillDesc": { - "message": "Auto-fill ang huling ginamit na login para sa kasalukuyang website" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Gumawa at kopyahin ang bagong random na password sa clipboard" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Nilikha", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Kasaysayan ng Password" }, @@ -1533,6 +1742,10 @@ "message": "Base domain", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Pangalan ng domain", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Match Detection - Pagtuklas ng Pares", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Kamalian ng Default", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Mga pagpipilian sa toggle" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Walang mga password na i-list." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Alisin" }, @@ -1677,6 +1899,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" }, @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Hindi matugunan ng iyong bagong pangunahing password ang mga kinakailangan ng patakaran." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "Hinarang ng isang patakaran ng organisasyon ang pag-import ng mga item sa iyong vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Inilayo na Domain" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ ay hindi isang valid domain", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Ipadala", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Protektado ng Password" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Kopyahin ang Link ng Padala", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Ipadala na nilikha", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Ipadala na nai-save", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Kailangan ang pag verify ng email" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Kailangan mong i-verify ang iyong email upang gamitin ang tampok na ito. Maaari mong i-verify ang iyong email sa web vault." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Awtomatikong pagpapatala" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Mga setting ng auto-fill" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" + }, "autofillShortcut": { "message": "Keyboard shortcut para sa auto-fill" }, - "autofillShortcutNotSet": { - "message": "Hindi naka-set ang shortcut ng auto-fill. Baguhin ito sa mga setting ng browser." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "Ang shortcut ng auto-fill ay: $COMMAND$. Baguhin ito sa mga setting ng browser.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 97ebd80b1d1..f80a5959ef3 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Identifiez-vous ou créez un nouveau compte pour accéder à votre coffre sécurisé." }, + "inviteAccepted": { + "message": "Invitation acceptée" + }, "createAccount": { "message": "Créer un compte" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Terminer la création de votre compte en définissant un mot de passe" }, - "login": { - "message": "Se connecter" - }, "enterpriseSingleSignOn": { "message": "Portail de connexion unique d'entreprise" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Indice du mot de passe principal (facultatif)" }, + "joinOrganization": { + "message": "Rejoindre l'organisation" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Terminer de rejoindre cette organisation en configurant un mot de passe principal." + }, "tab": { "message": "Onglet" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Copier le code de sécurité" }, + "copyName": { + "message": "Copier le nom" + }, + "copyCompany": { + "message": "Copier l'entreprise" + }, + "copySSN": { + "message": "Copier le numéro de sécurité sociale" + }, + "copyPassportNumber": { + "message": "Copier le numéro de passeport" + }, + "copyLicenseNumber": { + "message": "Copier la plaque d'immatriculation" + }, "autoFill": { "message": "Saisie automatique" }, @@ -150,7 +171,7 @@ "message": "Connectez-vous à votre coffre" }, "autoFillInfo": { - "message": "Il n'y a pas d'identifiants disponibles pour la saisie automatique de l'onglet actuel du navigateur." + "message": "Il n'y a aucun identifiant disponible pour le remplissage automatique de l'onglet actuel du navigateur." }, "addLogin": { "message": "Ajouter un identifiant" @@ -280,6 +301,24 @@ "editFolder": { "message": "Modifier le dossier" }, + "newFolder": { + "message": "Nouveau dossier" + }, + "folderName": { + "message": "Nom de dossier" + }, + "folderHintText": { + "message": "Imbriquer un dossier en ajoutant le nom du dossier parent suivi d'un \"/\". Par exemple : Social/Forums" + }, + "noFoldersAdded": { + "message": "Pas de dossier ajouté" + }, + "createFoldersToOrganize": { + "message": "Créer des dossiers pour organiser les éléments de votre coffre" + }, + "deleteFolderPermanently": { + "message": "Êtes-vous sûr de vouloir supprimer définitivement ce dossier ?" + }, "deleteFolder": { "message": "Supprimer le dossier" }, @@ -345,16 +384,56 @@ "message": "Longueur minimale du mot de passe" }, "uppercase": { - "message": "Majuscules (A-Z)" + "message": "Majuscules (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Minuscules (a-z)" + "message": "Minuscules (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Chiffres (0-9)" + "message": "Chiffres (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Caractères spéciaux (!@#$%^&*)" + "message": "Caractères spéciaux (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Inclure", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Inclure des caractères majuscules", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Inclure des caractères minuscules", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Inclure des nombres", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Inclure des caractères spéciaux", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Nombre de mots" @@ -376,7 +455,12 @@ "message": "Minimum de caractères spéciaux" }, "avoidAmbChar": { - "message": "Éviter les caractères ambigus" + "message": "Éviter les caractères ambigus", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Éviter les caractères ambigus", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Rechercher dans le coffre" @@ -556,6 +640,18 @@ "security": { "message": "Sécurité" }, + "confirmMasterPassword": { + "message": "Confirmer le mot de passe principal" + }, + "masterPassword": { + "message": "Mot de passe principal" + }, + "masterPassImportant": { + "message": "Votre mot de passe principal ne peut pas être récupéré si vous l'oubliez !" + }, + "masterPassHintLabel": { + "message": "Indice du mot de passe principal" + }, "errorOccurred": { "message": "Une erreur est survenue" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Votre nouveau compte a été créé ! Vous pouvez maintenant vous authentifier." }, + "newAccountCreated2": { + "message": "Votre nouveau compte a été créé !" + }, + "youHaveBeenLoggedIn": { + "message": "Vous avez été connecté !" + }, "youSuccessfullyLoggedIn": { "message": "Vous vous êtes connecté avec succès" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Le code de vérification est requis." }, + "webauthnCancelOrTimeout": { + "message": "L'authentification a été annulée ou a pris trop de temps. Veuillez réessayer." + }, "invalidVerificationCode": { "message": "Code de vérification invalide" }, @@ -613,7 +718,7 @@ } }, "autofillError": { - "message": "Impossible de saisir automatiquement l'élément sélectionné sur cette page. Essayez plutôt le copier-coller." + "message": "Impossible de remplir automatiquement le site sélectionné sur cette page. Copiez/collez plutôt votre nom d'utilisateur et/ou votre mot de passe." }, "totpCaptureError": { "message": "Impossible de scanner le QR code à partir de la page web actuelle" @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scanner le QR code de l'authentificateur à partir de la page web actuelle" }, + "totpHelperTitle": { + "message": "Rendre la vérification en deux étapes transparente" + }, + "totpHelper": { + "message": "Bitwarden peut stocker et remplir des codes de vérification en 2 étapes. Copiez et collez la clé dans ce champ." + }, + "totpHelperWithCapture": { + "message": "Bitwarden peut stocker et remplir des codes de vérification en 2 étapes. Sélectionnez l'icône caméra pour prendre une capture d'écran du code QR de l'authentificateur de ce site Web, ou copiez et collez la clé dans ce champ." + }, + "learnMoreAboutAuthenticators": { + "message": "En savoir plus sur les authentificateurs" + }, "copyTOTP": { "message": "Copier la clé Authenticator (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Votre session a expiré." }, + "logIn": { + "message": "Se connecter" + }, + "restartRegistration": { + "message": "Redémarrer l'inscription" + }, + "expiredLink": { + "message": "Lien expiré" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Veuillez redémarrer votre inscription ou essayez de vous connecter." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Vous avez peut-être déjà un compte" + }, "logOutConfirmation": { "message": "Êtes-vous sûr de vouloir vous déconnecter ?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Nouvel URI" }, + "addDomain": { + "message": "Ajouter un domaine", + "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": { "message": "Élément ajouté" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Demander d'ajouter un identifiant" }, + "vaultSaveOptionsTitle": { + "message": "Enregistrer dans les options de coffre" + }, "addLoginNotificationDesc": { "message": "Demander d'ajouter un élément si aucun n'est trouvé dans votre coffre." }, "addLoginNotificationDescAlt": { "message": "Demande l'ajout d'un élément si celui-ci n'est pas trouvé dans votre coffre. S'applique à tous les comptes connectés." }, + "showCardsInVaultView": { + "message": "Afficher les cartes de paiement en tant que suggestions de saisie automatique dans la vue du coffre" + }, "showCardsCurrentTab": { "message": "Afficher les cartes de paiement sur la Page d'onglet" }, "showCardsCurrentTabDesc": { "message": "Liste les éléments des cartes de paiement sur la Page d'onglet pour faciliter la saisie automatique." }, + "showIdentitiesInVaultView": { + "message": "Afficher les identités en tant que suggestions de saisie automatique dans la vue du coffre" + }, "showIdentitiesCurrentTab": { "message": "Afficher les identités sur la Page d'onglet" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Détection de correspondance URI par défaut", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Choisit la manière dont la détection des correspondances URI est gérée par défaut pour les connexions lors d'actions telles que la saisie automatique." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 Go de stockage chiffré pour les fichiers joints." }, + "premiumSignUpEmergency": { + "message": "Accès d'urgence." + }, "premiumSignUpTwoStepOptions": { "message": "Options de connexion propriétaires à deux facteurs telles que YubiKey et Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Vous pouvez acheter une adhésion Premium sur le coffre web de bitwarden.com. Voulez-vous visiter le site web maintenant ?" }, + "premiumPurchaseAlertV2": { + "message": "Vous pouvez acheter la version Premium depuis les paramètres de votre compte dans l'application web Bitwarden." + }, "premiumCurrentMember": { "message": "Vous êtes un membre Premium !" }, "premiumCurrentMemberThanks": { "message": "Merci de soutenir Bitwarden." }, + "premiumFeatures": { + "message": "Mettre à niveau à la version Premium et recevez :" + }, "premiumPrice": { "message": "Tout pour seulement $PRICE$/an !", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "Tout pour seulement $PRICE$ /an !", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Actualisation terminée" }, @@ -1106,17 +1269,17 @@ "message": "Application d'authentification" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Entrez un code généré par une application d'authentification comme Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP Security Key" + "message": "Clé de sécurité OTP de Yubico" }, "yubiKeyDesc": { "message": "Utiliser une YubiKey pour accéder à votre compte. Fonctionne avec les appareils YubiKey 4, 4 Nano, 4C et NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Entrez un code généré par Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1133,7 +1296,7 @@ "message": "Courriel" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Entrez le code envoyé à votre adresse courriel." }, "selfHostedEnvironment": { "message": "Environnement auto-hébergé" @@ -1179,9 +1342,18 @@ }, "showAutoFillMenuOnFormFields": { "message": "Afficher le menu de saisie automatique dans les champs d'un formulaire", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Suggestions de saisie automatique" + }, + "showInlineMenuLabel": { + "message": "Afficher les suggestions de saisie automatique dans les champs d'un formulaire" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Afficher les suggestions lorsque l'icône est sélectionnée" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "S'applique à tous les comptes connectés." }, "turnOffBrowserBuiltInPasswordManagerSettings": { @@ -1202,15 +1374,34 @@ "message": "Lorsque l'icône de saisie automatique est sélectionnée", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Saisie automatique lors du chargement de la page" + }, "enableAutoFillOnPageLoad": { "message": "Saisir automatiquement au chargement de la page" }, "enableAutoFillOnPageLoadDesc": { "message": "Si un formulaire de connexion est détecté, il sera saisi automatiquement lors du chargement de la page web." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Attention :$CLOSETAG$ Les sites web compromis ou non fiables peuvent exploiter la saisie automatique lors du chargement de la page.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "les sites web compromis ou non fiables peuvent exploiter la saisie automatique au chargement de la page." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "En savoir plus sur les risques" + }, "learnMoreAboutAutofill": { "message": "En savoir plus sur la saisie automatique" }, @@ -1238,9 +1429,15 @@ "commandOpenSidebar": { "message": "Ouvrir le coffre dans la barre latérale" }, - "commandAutofillDesc": { + "commandAutofillLoginDesc": { "message": "Saisir automatiquement le dernier identifiant utilisé pour le site web actuel" }, + "commandAutofillCardDesc": { + "message": "Saisir automatiquement la dernière carte utilisée pour le site web actuel" + }, + "commandAutofillIdentityDesc": { + "message": "Saisir automatiquement la dernière identité utilisée pour le site web actuel" + }, "commandGeneratePasswordDesc": { "message": "Générer et copier un nouveau mot de passe aléatoire dans le presse-papiers." }, @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Booléen" }, + "cfTypeCheckbox": { + "message": "Case à cocher" + }, "cfTypeLinked": { "message": "Lié", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "Voir les $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Historique des mots de passe" }, @@ -1533,6 +1742,10 @@ "message": "Domaine de base", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Domaine de base (recommandé)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Nom de domaine", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Détection de correspondance", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Détection de correspondance par défaut", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Options de basculement" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Aucun mot de passe à afficher." }, + "clearHistory": { + "message": "Effacer l'historique" + }, + "noPasswordsToShow": { + "message": "Aucun mot de passe à afficher" + }, + "noRecentlyGeneratedPassword": { + "message": "Vous n'avez pas généré de mot de passe récemment" + }, "remove": { "message": "Supprimer" }, @@ -1677,6 +1899,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Une ou plusieurs politiques de sécurité de l'organisation affectent les paramètres de votre générateur." }, + "passwordGenerator": { + "message": "Générateur de mot de passe" + }, + "usernameGenerator": { + "message": "Générateur de nom d'utilisateur" + }, + "useThisPassword": { + "message": "Utiliser ce mot de passe" + }, + "useThisUsername": { + "message": "Utiliser ce nom d'utilisateur" + }, + "securePasswordGenerated": { + "message": "Mot de passe sécurisé généré ! N'oubliez pas aussi de mettre à jour votre mot de passe sur le site Web." + }, + "useGeneratorHelpTextPartOne": { + "message": "Utiliser le générateur", + "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": "pour créer un mot de passe fort unique", + "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" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Votre nouveau mot de passe principal ne répond pas aux exigences de politique de sécurité." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Obtenez des conseils, des annonces et des opportunités de recherche de la part de Bitwarden dans votre boîte de réception." }, "unsubscribe": { "message": "Se désabonner" @@ -1874,6 +2119,12 @@ "nativeMessagingWrongUserTitle": { "message": "Erreur de correspondance entre les comptes" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Le déverrouillage biométrique a échoué. La clé secrète biométrique n'a pas réussi à déverrouiller le coffre. Veuillez essayer de configurer à nouveau la biométrie." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Non-concordance des clés biométriques" + }, "biometricsNotEnabledTitle": { "message": "Le déverrouillage biométrique n'est pas activé" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Veuillez déverrouiller cet utilisateur dans l'application de bureau et réessayer." }, + "biometricsNotAvailableTitle": { + "message": "Déverrouillage biométrique indisponible" + }, + "biometricsNotAvailableDesc": { + "message": "Le déverrouillage biométrique est actuellement indisponible. Veuillez réessayer plus tard." + }, "biometricsFailedTitle": { "message": "Le déverrouillage biométique a échoué\n" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "Une politique d'organisation a bloqué l'import d'éléments dans votre coffre personel." }, + "domainsTitle": { + "message": "Domaines", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Domaines exclus" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden ne demandera pas d'enregistrer les détails de connexion pour ces domaines pour tous les comptes connectés. Vous devez actualiser la page pour que les modifications prennent effet." }, + "websiteItemLabel": { + "message": "Site web $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ n'est pas un domaine valide", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Changements de domaines exclus enregistrés" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Protégé par un mot de passe" }, + "copyLink": { + "message": "Copier le lien" + }, "copySendLink": { "message": "Copier le lien du Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send créé", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send créé avec succès !", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "Le Send est disponible à toute personne ayant le lien durant les $DAYS$ prochains jours.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Lien Send copié", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send sauvegardé", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Vérification de courriel requise" }, + "emailVerifiedV2": { + "message": "Courriel vérifié" + }, "emailVerificationRequiredDesc": { "message": "Vous devez vérifier votre courriel pour utiliser cette fonctionnalité. Vous pouvez vérifier votre courriel dans le coffre web." }, @@ -2179,6 +2476,9 @@ "updateWeakMasterPasswordWarning": { "message": "Votre mot de passe principal ne répond pas aux exigences de politique de sécurité de cette organisation. Pour 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": "Votre organisation a désactivé le déchiffrement de votre appareil de confiance. Veuillez configurer un mot de passe principal pour accéder à votre coffre." + }, "resetPasswordPolicyAutoEnroll": { "message": "Inscription automatique" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Paramètres de saisie automatique" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Raccourci de saisie automatique" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Modifier le raccourci" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Gérer les raccourcis" + }, "autofillShortcut": { "message": "Raccourci clavier de saisie automatique" }, - "autofillShortcutNotSet": { - "message": "Le raccourci de saisie automatique n'est pas défini. Changez-le dans les paramètres du navigateur." + "autofillLoginShortcutNotSet": { + "message": "Le raccourci de saisie automatique de l'identifiant n'est pas configuré. Changez-le dans les paramètres du navigateur." }, - "autofillShortcutText": { - "message": "Le raccourci de saisie automatique est : $COMMAND$. Changez-le dans les paramètres du navigateur.", + "autofillLoginShortcutText": { + "message": "Le raccourci de saisie automatique de l'identifiant est $COMMAND$. Gérez tous les raccourcis dans les paramètres du navigateur.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Appareil de confiance" }, + "sendsNoItemsTitle": { + "message": "Pas de Send actif", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Utilisez Send pour partager en toute sécurité des informations chiffrées avec tout le monde.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Saisie requise." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 champ nécessite votre attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ champs nécessitent votre attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Sélectionner --" }, @@ -2879,18 +3208,18 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Les éléments pour lesquels le mot de passe principal est redemandé ne peuvent pas être remplis automatiquement lors du chargement de la page. La saisie automatique au chargement de la page est désactivée.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "La saisie automatique au chargement de la page est configuré selon les paramètres par défaut.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Désactivez la resaisie du mot de passe maître pour éditer ce champ", "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "Basculer la navigation latérale" }, "skipToContent": { "message": "Accéder directement au contenu" @@ -2911,10 +3240,18 @@ "message": "Déverrouillez votre compte pour afficher les identifiants correspondants", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Déverrouillez votre compte pour afficher les suggestions de saisie automatique", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Déverrouiller le compte", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Déverrouiller votre compte, s'ouvre dans une nouvelle fenêtre", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Remplir les identifiants pour", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Ajouter un nouvel élément de coffre", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "Nouvel identifiant", + "description": "Button text to display within inline menu when there are no matching items on a login field" + }, + "addNewLoginItemAria": { + "message": "Ajouter un nouvel élément de connexion au coffre, s'ouvre dans une nouvelle fenêtre", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "Nouvelle carte de paiement", + "description": "Button text to display within inline menu when there are no matching items on a credit card field" + }, + "addNewCardItemAria": { + "message": "Ajouter un nouvel élément de carte au coffre, s'ouvre dans une nouvelle fenêtre", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "Nouvelle identité", + "description": "Button text to display within inline menu when there are no matching items on an identity field" + }, + "addNewIdentityItemAria": { + "message": "Ajouter un nouvel élément d'identité au coffre, s'ouvre dans une nouvelle fenêtre", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Menu de saisie automatique de Bitwarden disponible. Appuyez sur la touche Flèche bas pour sélectionner.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Erreur de connexion avec le service Duo. Utilisez une autre méthode de connexion en deux étapes ou contactez Duo pour obtenir de l'aide." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Lancez DUO et suivez les étapes pour terminer la connexion." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Mot de passe du fichier incorrect, veuillez utiliser le mot de passe saisi lors de l'exportation du fichier." }, - "importDestination": { - "message": "Importer la destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "En savoir plus sur vos options d'importation" @@ -3108,7 +3472,7 @@ "message": "Confirmez le mot de passe du fichier" }, "exportSuccess": { - "message": "Vault data exported" + "message": "Données du coffre exportées" }, "typePasskey": { "message": "Clé d'identification (passkey)" @@ -3122,8 +3486,8 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Vérification requise par le site initiateur. Cette fonctionnalité n'est pas encore implémentée pour les comptes sans mot de passe principal." }, - "logInWithPasskey": { - "message": "Se connecter avec une clé d'identification (passkey) ?" + "logInWithPasskeyQuestion": { + "message": "Se connecter avec une clé d'accès ?" }, "passkeyAlreadyExists": { "message": "Une clé d'identification (passkey) existe déjà pour cette application." @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "Vous n'avez pas d'identifiant correspondant à ce site." }, + "noMatchingLoginsForSite": { + "message": "Aucun identifiant correspondant pour ce site" + }, "confirm": { "message": "Confirmer" }, @@ -3143,8 +3510,11 @@ "savePasskeyNewLogin": { "message": "Enregistrer la clé d'identification (passkey) comme nouvel identifiant" }, - "choosePasskey": { - "message": "Choisissez cette clé d'identification (passkey) pour l'enregistrer avec cet identifiant" + "chooseCipherForPasskeySave": { + "message": "Choisissez un identifiant ou enregistrer cette clé d'accès" + }, + "chooseCipherForPasskeyAuth": { + "message": "Choisissez une clé d'accès pour vous connecter" }, "passkeyItem": { "message": "Élément clé d'identification (passkey)" @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,6 +3659,30 @@ "message": "Formats communs", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continuer vers les paramètres du navigateur ?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continuer vers le centre d'aide ?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" + }, + "confirmContinueToHelpCenterPasswordManagementContent": { + "message": "Modifiez les paramètres de saisie automatique et de gestion des mots de passe de votre navigateur.", + "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": "Vous pouvez afficher et définir les raccourcis d'extension dans les paramètres de votre navigateur.", + "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": "Modifiez les paramètres de saisie automatique et de gestion des mots de passe de votre navigateur.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" + }, + "confirmContinueToBrowserKeyboardShortcutSettingsContent": { + "message": "Vous pouvez afficher et définir les raccourcis d'extension dans les paramètres de votre navigateur.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Faire de Bitwarden votre gestionnaire de mots de passe par défaut ?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" @@ -3317,10 +3711,18 @@ "message": "Identifiants enregistrés avec succès !", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Mot de passe enregistré !", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Identifiants mis à jour avec succès !", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Mot de passe mis à jour !", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Erreur lors de l'enregistrement des identifiants. Consultez la console pour plus de détails.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Clé d'identification (passkey) retirée" }, - "unassignedItemsBannerNotice": { - "message": "Remarque : Les éléments d'organisation non assignés ne sont plus visibles dans la vue Tous les coffres et ne sont maintenant accessibles que via la Console Admin." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Remarque : À partir du 16 mai 2024, les éléments d'organisation non assignés ne seront plus visibles dans votre vue Tous les coffres sur les appareils et ne seront maintenant accessibles que via la Console Admin." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Ajouter ces éléments à une collection depuis la", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "pour les rendre visibles.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Suggestions de saisie automatique" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Enregistrez un élément de connexion à remplir automatiquement pour ce site" }, "yourVaultIsEmpty": { "message": "Votre coffre-fort est vide" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Saisie automatique - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "Aucune valeur à copier" }, - "assignCollections": { - "message": "Assigner une collection" + "assignToCollections": { + "message": "Assigner aux collections" }, "copyEmail": { "message": "Copier l'email" @@ -3493,13 +3881,13 @@ "message": "Eléments sans dossier" }, "itemDetails": { - "message": "Item details" + "message": "Détails de l'élément" }, "itemName": { - "message": "Item name" + "message": "Nom de l’élément" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Vous ne pouvez pas supprimer des collections avec les autorisations d'affichage uniquement : $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,26 +3899,44 @@ "message": "L'organisation est désactivée" }, "owner": { - "message": "Owner" + "message": "Propriétaire" }, "selfOwnershipLabel": { - "message": "You", + "message": "Vous", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Les éléments des Organisations désactivées ne sont pas accessibles. Contactez le propriétaire de votre Organisation pour obtenir de l'aide." }, + "additionalInformation": { + "message": "Informations supplémentaires" + }, + "itemHistory": { + "message": "Historique de l'élément" + }, + "lastEdited": { + "message": "Dernière modification" + }, + "ownerYou": { + "message": "Propriétaire : Vous" + }, + "linked": { + "message": "Lié" + }, + "copySuccessful": { + "message": "Copié avec succès" + }, "upload": { - "message": "Upload" + "message": "Téléverser" }, "addAttachment": { - "message": "Add attachment" + "message": "Ajouter une pièce jointe" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "La taille maximale du fichier est de 500 Mo" }, "deleteAttachmentName": { - "message": "Delete attachment $NAME$", + "message": "Supprimer la pièce jointe $NAME$", "placeholders": { "name": { "content": "$1", @@ -3539,7 +3945,7 @@ } }, "downloadAttachmentName": { - "message": "Download $NAME$", + "message": "Télécharger $NAME$", "placeholders": { "name": { "content": "$1", @@ -3548,15 +3954,389 @@ } }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Are you sure you want to permanently delete this attachment?" + "message": "Êtes-vous sûr de vouloir supprimer définitivement cette pièce jointe ?" }, "premium": { "message": "Premium" }, "freeOrgsCannotUseAttachments": { - "message": "Free organizations cannot use attachments" + "message": "Les organisations gratuites ne peuvent pas utiliser de pièces jointes" }, "filters": { "message": "Filtres" + }, + "personalDetails": { + "message": "Données personnelles" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Informations de contact" + }, + "downloadAttachment": { + "message": "Télécharger - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "numéro de la carte se termine par", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Identifiants de connexion" + }, + "authenticatorKey": { + "message": "Clé d'authentification" + }, + "autofillOptions": { + "message": "Options de saisie automatique" + }, + "websiteUri": { + "message": "Site web (URI)" + }, + "websiteUriCount": { + "message": "Site web (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": "Site web ajouté" + }, + "addWebsite": { + "message": "Ajouter le site web" + }, + "deleteWebsite": { + "message": "Supprimer le site web" + }, + "defaultLabel": { + "message": "Par défaut ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Saisir automatiquement lors du chargement de la page ?" + }, + "cardExpiredTitle": { + "message": "Carte de paiement expirée" + }, + "cardExpiredMessage": { + "message": "Si vous l'avez renouvelée, mettez à jour les informations de la carte de paiement" + }, + "cardDetails": { + "message": "Détails de la carte de paiement" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Activer les animations" + }, + "addAccount": { + "message": "Ajouter un compte" + }, + "loading": { + "message": "Chargement" + }, + "data": { + "message": "Données" + }, + "passkeys": { + "message": "Clés d'accès", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Mots de passe", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Se connecter avec une clé d'accès", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, + "assign": { + "message": "Assigner" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Seuls les membres de l'organisation ayant accès à ces collections pourront voir l'élément." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Seuls les membres de l'organisation ayant accès à ces collections pourront voir les éléments." + }, + "bulkCollectionAssignmentWarning": { + "message": "Vous avez sélectionné $TOTAL_COUNT$ éléments. Vous ne pouvez pas mettre à jour $READONLY_COUNT$ de ces éléments parce que vous n'avez pas les autorisations pour les éditer.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Ajouter un champ" + }, + "add": { + "message": "Ajouter" + }, + "fieldType": { + "message": "Type du champ" + }, + "fieldLabel": { + "message": "Intitulé du champ" + }, + "textHelpText": { + "message": "Utiliser des champs de texte pour les données telles que les questions de sécurité" + }, + "hiddenHelpText": { + "message": "Utiliser des champs cachés pour des données sensibles telles qu'un mot de passe" + }, + "checkBoxHelpText": { + "message": "Utilisez les cases à cocher si vous souhaitez saisir automatiquement la case à cocher d'un formulaire, tel qu'un courriel de rappel" + }, + "linkedHelpText": { + "message": "Utilisez un champ lié lorsque vous rencontrez des problèmes de saisie automatique pour un site Web spécifique." + }, + "linkedLabelHelpText": { + "message": "Entrez l'identifiant html, le nom, l'étiquette aria ou l'espace réservé du champ." + }, + "editField": { + "message": "Éditer le champ" + }, + "editFieldLabel": { + "message": "Éditer $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Supprimer $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ ajouté", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Réorganiser $LABEL$. Utilisez les flèches de votre clavier pour déplacer l'élément vers le haut ou vers le bas.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Sélectionnez les collections à assigner" + }, + "personalItemTransferWarningSingular": { + "message": "1 élément sera transféré définitivement à l'organisation sélectionnée. Vous ne serez plus le propriétaire de cet élément." + }, + "personalItemsTransferWarningPlural": { + "message": "Les éléments $PERSONAL_ITEMS_COUNT$ seront transférés de façon permanente à l'organisation sélectionnée. Vous ne serez plus le propriétaire de ces éléments.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 élément sera transféré définitivement à $ORG$. Vous ne serez plus le propriétaire de cet élément.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "Les éléments $PERSONAL_ITEMS_COUNT$ seront transférés à $ORG$ de façon permanente. Vous ne serez plus propriétaire de ces éléments.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Collections assignées avec succès" + }, + "nothingSelected": { + "message": "Vous n'avez rien sélectionné." + }, + "movedItemsToOrg": { + "message": "Les éléments sélectionnés ont été déplacés vers $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Éléments déplacés vers $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Élément déplacé vers $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Emplacement de l'élément" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden a un nouveau look !" + }, + "bitwardenNewLookDesc": { + "message": "Il est plus facile et plus intuitif que jamais de remplir automatiquement les champs et d'effectuer des recherches à partir de l'onglet \"Coffre\". Jetez un coup d'œil !" + }, + "accountActions": { + "message": "Actions du compte" + }, + "showNumberOfAutofillSuggestions": { + "message": "Afficher le nombre de suggestions de saisie automatique d'identifiant sur l'icône d'extension" + }, + "systemDefault": { + "message": "Par défaut" + }, + "enterprisePolicyRequirementsApplied": { + "message": "Les exigences de la politique d'entreprise ont été appliquées à ce paramètre" + }, + "fileSavedToDevice": { + "message": "Fichier enregistré sur l'appareil. Gérez à partir des téléchargements de votre appareil." + }, + "showCharacterCount": { + "message": "Afficher le nombre de caractères" + }, + "hideCharacterCount": { + "message": "Cacher le nombre de caractères" + }, + "itemsInTrash": { + "message": "Éléments dans la corbeille" + }, + "noItemsInTrash": { + "message": "Aucun élément dans la corbeille" + }, + "noItemsInTrashDesc": { + "message": "Les éléments que vous supprimez apparaîtront ici et seront définitivement supprimés au bout de 30 jours" + }, + "trashWarning": { + "message": "Les éléments qui se trouvent dans la corbeille depuis plus de 30 jours sont automatiquement supprimés" + }, + "restore": { + "message": "Restaurer" + }, + "deleteForever": { + "message": "Supprimer définitivement" + }, + "noEditPermissions": { + "message": "Vous n'êtes pas autorisé à modifier cet élément" } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 57e1d88ef40..247542ec192 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Rexístrate ou crea unha nova conta para acceder á túa caixa forte." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "Crea unha conta" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "Iniciar sesión" - }, "enterpriseSingleSignOn": { "message": "Inicio de sesión único empresarial" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Pista do contrasinal mestre (opcional)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "Separador" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Copiar código de seguranza" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "Auto-encher" }, @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "Lonxitude mínima do contrasinal" }, "uppercase": { - "message": "Maiúsculas (A-Z)" + "message": "Maiúsculas (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Minúsculas (a-z)" + "message": "Minúsculas (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Números (0-9)" + "message": "Números (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Caracteres especiais (!@#$%^&*)" + "message": "Caracteres especiais (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Número de palabras" @@ -376,7 +455,12 @@ "message": "Mínimo de caracteres especiais" }, "avoidAmbChar": { - "message": "Evitar caracteres ambiguos" + "message": "Evitar caracteres ambiguos", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Buscar na caixa forte" @@ -556,6 +640,18 @@ "security": { "message": "Seguridade" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Produciuse un erro" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "A túa nova conta foi creada! Podes iniciar sesión agora." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "Iniciaches sesión correctamente" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "É preciso código de verificación." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Código de verificación non válido" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Escanea o código QR autenticador da páxina web actual" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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": "Copiar clave de autenticación (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "A túa sesión caducou." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Nova URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "Elemento engadido" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Solicita engadir inicio de sesión" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "Ask to add an item if one isn't found in your vault." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Amosar tarxetas no separador" }, "showCardsCurrentTabDesc": { "message": "Lista os elementos de tarxeta no separador para fácil auto-completado." }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" + }, "showIdentitiesCurrentTab": { "message": "Mostrar identidades no separador" }, @@ -791,7 +936,7 @@ "message": "Actualizar" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Desbloquear" @@ -810,10 +955,10 @@ }, "defaultUriMatchDetection": { "message": "Default URI match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { - "message": "Choose the default way that URI match detection is handled for logins when performing actions such as auto-fill." + "message": "Choose the default way that URI match detection is handled for logins when performing actions such as autofill." }, "theme": { "message": "Tema" @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a Premium member!" }, "premiumCurrentMemberThanks": { "message": "Thank you for supporting Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "All for just $PRICE$ /year!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Refresh complete" }, @@ -1028,7 +1191,7 @@ "message": "Copy TOTP automatically" }, "disableAutoTotpCopyDesc": { - "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login." + "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you autofill the login." }, "enableAutoBiometricsPrompt": { "message": "Ask for biometrics on launch" @@ -1178,14 +1341,23 @@ "message": "URLs do entorno gardadas" }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,38 +1371,57 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "enableAutoFillOnPageLoadDesc": { - "message": "If a login form is detected, auto-fill when the web page loads." + "message": "If a login form is detected, autofill when the web page loads." + }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Compromised or untrusted websites can exploit autofill on page load." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" }, "learnMoreAboutAutofill": { - "message": "Learn more about auto-fill" + "message": "Learn more about autofill" }, "defaultAutoFillOnPageLoad": { "message": "Default autofill setting for login items" }, "defaultAutoFillOnPageLoadDesc": { - "message": "You can turn off auto-fill on page load for individual login items from the item's Edit view." + "message": "You can turn off autofill on page load for individual login items from the item's Edit view." }, "itemAutoFillOnPageLoad": { - "message": "Auto-fill on page load (if set up in Options)" + "message": "Autofill on page load (if set up in Options)" }, "autoFillOnPageLoadUseDefault": { "message": "Use default setting" }, "autoFillOnPageLoadYes": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "autoFillOnPageLoadNo": { - "message": "Do not auto-fill on page load" + "message": "Do not autofill on page load" }, "commandOpenPopup": { "message": "Open vault popup" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Open vault in sidebar" }, - "commandAutofillDesc": { - "message": "Auto-fill the last used login for the current website" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Generate and copy a new random password to the clipboard" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Booleano" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Vinculado", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Historial de contrasinais" }, @@ -1533,6 +1742,10 @@ "message": "Dominio base", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Nome de dominio", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Match detection", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Default match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Toggle options" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Non hai contrasinais que listar." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Eliminar" }, @@ -1677,6 +1899,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" }, @@ -1716,16 +1961,16 @@ "message": "Timeout action confirmation" }, "autoFillAndSave": { - "message": "Auto-fill and save" + "message": "Autofill and save" }, "fillAndSave": { "message": "Fill and save" }, "autoFillSuccessAndSavedUri": { - "message": "Item auto-filled and URI saved" + "message": "Item autofilled and URI saved" }, "autoFillSuccess": { - "message": "Item auto-filled " + "message": "Item autofilled " }, "insecurePageWarning": { "message": "Warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page." @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ is not a valid domain", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Password protected" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Copiar ligazón Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send creado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send gardado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2606,10 +2906,10 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { - "message": "How to auto-fill" + "message": "How to autofill" }, "autofillSelectInfoWithCommand": { "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", @@ -2627,16 +2927,25 @@ "message": "Got it" }, "autofillSettings": { - "message": "Auto-fill settings" + "message": "Autofill settings" + }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Aviso: O 16 de maio de 2024, os elementos de organización non asignados non serán visíbeis na vista de Todas as caixas fortes e só serán accesíbeis a través da Consola de Administrador." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index b1cbfaec626..02a6842efa2 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "צור חשבון חדש או התחבר כדי לגשת לכספת המאובטחת שלך." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "צור חשבון" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "התחבר" - }, "enterpriseSingleSignOn": { "message": "כניסה ארגונית אחידה" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "רמז לסיסמה ראשית (אופציונאלי)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "לשונית" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "העתק קוד אבטחה" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "השלמה אוטומטית" }, @@ -280,6 +301,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,16 +384,56 @@ "message": "Minimum password length" }, "uppercase": { - "message": "Uppercase (A-Z)" + "message": "Uppercase (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Lowercase (a-z)" + "message": "Lowercase (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numbers (0-9)" + "message": "Numbers (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Special characters (!@#$%^&*)" + "message": "Special characters (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "מספר מילים" @@ -376,7 +455,12 @@ "message": "מינימום תוים מיוחדים" }, "avoidAmbChar": { - "message": "המנע מאותיות ותוים דומים" + "message": "המנע מאותיות ותוים דומים", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "חיפוש בכספת" @@ -556,6 +640,18 @@ "security": { "message": "אבטחה" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "אירעה שגיאה" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "החשבון שלך נוצר בהצלחה! כעת ניתן להכנס למערכת." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "נדרש קוד אימות." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "קוד אימות שגוי" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "תוקף החיבור שלך הסתיים." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "האם אתה בטוח שברצונך להתנתק?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "כתובת חדשה" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "פריט שהתווסף" }, @@ -737,23 +873,32 @@ "enableAddLoginNotification": { "message": "Ask to add login" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "ההודעה \"שמור פרטי כניסה\" מופיעה בכל פעם שתכנס לאתר חדש בפעם הראשונה." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Show cards on Tab page" }, "showCardsCurrentTabDesc": { - "message": "List card items on the Tab page for easy auto-fill." + "message": "List card items on the Tab page for easy autofill." + }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" }, "showIdentitiesCurrentTabDesc": { - "message": "List identity items on the Tab page for easy auto-fill." + "message": "List identity items on the Tab page for easy autofill." }, "clearClipboard": { "message": "נקה לוח העתקות", @@ -791,7 +936,7 @@ "message": "כן, עדכן עכשיו" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Unlock" @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "ברירת מחדל לזיהוי התאמת כתובות", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "בחר את שיטת ברירת המחדל עבור זיהוי התאמת כתובות כשמבצעים פעולות השלמה אוטומטית." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 ג'יגה של מקום אחסון עבור קבצים מצורפים." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "באפשרותך לרכוש מנוי פרימיום בכספת באתר bitwarden.com. האם ברצונך לפתוח את האתר כעת?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "אתה מנוי פרימיום!" }, "premiumCurrentMemberThanks": { "message": "תודה על תמיכתך בBitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "הכל רק ב$PRICE$ לשנה!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "הרענון הושלם" }, @@ -1178,14 +1341,23 @@ "message": "כתובות הסביבה נשמרו." }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,20 +1371,39 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "הפעל השלמה אוטומטית בזמן טעינת העמוד" }, "enableAutoFillOnPageLoadDesc": { "message": "אם זוהה טופס כניסה, בצע אוטומטית מילוי-אוטומטי כשהעמוד נטען." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Compromised or untrusted websites can exploit autofill on page load." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" }, "learnMoreAboutAutofill": { - "message": "Learn more about auto-fill" + "message": "Learn more about autofill" }, "defaultAutoFillOnPageLoad": { "message": "הגדרת ברירת מחדל למילוי אוטומטי של פרטי התחברות" @@ -1230,7 +1421,7 @@ "message": "מילוי אוטומטי אחרי טעינת דפים" }, "autoFillOnPageLoadNo": { - "message": "Do not auto-fill on page load" + "message": "Do not autofill on page load" }, "commandOpenPopup": { "message": "פתיחת כספת בחלונית צפה" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "פתיחת כספת בסרגל צד" }, - "commandAutofillDesc": { - "message": "השתמש בהשלמה-האוטומטית האחרונה שבוצעה באתר זה." + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "צור והעתק סיסמה רנדומלית חדשה." @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "אמת או שקר" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "מקושר", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "היסטוריית סיסמאות" }, @@ -1533,6 +1742,10 @@ "message": "שם בסיס הדומיין", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "שם תחום", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "זיהוי התאמה", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "ברירת מחדל לזיהוי התאמות", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "הצגה\\הסתרה של אפשרויות" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "אין סיסמאות להצגה ברשימה." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "הסר" }, @@ -1677,6 +1899,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": "פעולה לביצוע בכספת בתום זמן החיבור" }, @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "הסיסמה הראשית החדשה השלך לא עומדת בדרישות המדיניות." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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": "אמצעי זיהוי ביומטרים לא מאופשרים" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ is not a valid domain", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Password protected" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Copy Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send created", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2606,10 +2906,10 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { - "message": "How to auto-fill" + "message": "How to autofill" }, "autofillSelectInfoWithCommand": { "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", @@ -2627,16 +2927,25 @@ "message": "Got it" }, "autofillSettings": { - "message": "Auto-fill settings" + "message": "Autofill settings" + }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "יעד ייבוא" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "תסדירים נפוצים", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 1c46e86753b..6b4d3922ff2 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -7,23 +7,23 @@ "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": "घर पर, काम पर या चलते-फिरते, बिटवर्डन आपके सभी पासवर्ड, पासकी और संवेदनशील जानकारी को आसानी से सुरक्षित रखता है", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "अपनी सुरक्षित तिजोरी में प्रवेश करने के लिए नया खाता बनाएं या लॉग इन करें।" }, + "inviteAccepted": { + "message": "आमंत्रण स्वीकृत" + }, "createAccount": { "message": "Create Account" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "मजबूत पासवर्ड सेट करें" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" - }, - "login": { - "message": "Log In" + "message": "पासवर्ड सेट करके अपना खाता निर्माण पूरा करें" }, "enterpriseSingleSignOn": { "message": "उद्यम एकल साइन-ऑन" @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Master Password Hint (optional)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "टैब" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Copy Security Code" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "स्वत:भरण" }, @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "न्यूनतम पासवर्ड लंबाई" }, "uppercase": { - "message": "बड़े अक्षर (A-Z)" + "message": "बड़े अक्षर (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "छोटे अक्षर (a-z)" + "message": "छोटे अक्षर (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "संख्या (0-9)" + "message": "संख्या (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "विशेष अक्षर (!@#$%^&*)" + "message": "विशेष अक्षर (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Number of Words" @@ -376,7 +455,12 @@ "message": "Minimum Special" }, "avoidAmbChar": { - "message": "Avoid Ambiguous Characters" + "message": "Avoid Ambiguous Characters", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "वॉल्ट खोजे" @@ -556,6 +640,18 @@ "security": { "message": "सुरक्षा" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "कोई ग़लती हुई।" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "आपका नया खाता बनाया गया है! अब आप लॉग इन कर सकते हैं।" }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "सत्यापन टोकन आवश्यक है" }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "सत्यापन कोड अवैध है" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "अपने लॉगिन सत्र समाप्त हो गया है।" }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "क्या आप वाकई लॉग आउट करना चाहते हैं?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "नया URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "जोड़ा गया आइटम" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "लॉगिन जोड़ने के लिए कहें" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "The \"Add Login Notification\" automatically prompts you to save new logins to your vault whenever you log into them for the first time." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "टैब पेज पर कार्ड दिखाएं" }, "showCardsCurrentTabDesc": { "message": "आसान ऑटो-फिल के लिए टैब पेज पर कार्ड आइटम सूचीबद्ध करें।" }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" + }, "showIdentitiesCurrentTab": { "message": "टैब पेज पर पहचान दिखाएं" }, @@ -791,7 +936,7 @@ "message": "Yes, Update Now" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Unlock" @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "डिफॉल्ट URI मैच डिटेक्शन", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "ऑटो-फिल जैसे कार्यों को करते समय लॉगिन के लिए URI मैच डिटेक्शन को संभाले जाने का डिफ़ॉल्ट तरीका चुनें। " @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB of encrypted file storage." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "आप bitwarden.com वेब वॉल्ट पर प्रीमियम सदस्यता खरीद सकते हैं।क्या आप अब वेबसाइट पर जाना चाहते हैं?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "आप एक प्रीमियम सदस्य हैं!" }, "premiumCurrentMemberThanks": { "message": "Thank you for supporting bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "All for just $PRICE$ /year!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "ताज़ा पूरा" }, @@ -1178,10 +1341,19 @@ "message": "पर्यावरण URL को बचाया गया है।" }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { @@ -1199,20 +1371,39 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "Enable Auto-fill On Page Load." }, "enableAutoFillOnPageLoadDesc": { "message": "यदि लॉगिन फॉर्म का पता चलता है, तो वेब पेज लोड होने पर स्वचालित रूप से ऑटो-फिल करें।" }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Compromised or untrusted websites can exploit autofill on page load." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" }, "learnMoreAboutAutofill": { - "message": "Learn more about auto-fill" + "message": "Learn more about autofill" }, "defaultAutoFillOnPageLoad": { "message": "लॉगिन आइटम के लिए डिफ़ॉल्ट ऑटोफिल सेटिंग" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "साइडबार में वॉल्ट खोले" }, - "commandAutofillDesc": { - "message": "Auto-fill the last used login for the current website." + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Generate and copy a new random password to the clipboard." @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "बूलियन" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "जुड़ा हुआ", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "पासवर्ड इतिहास" }, @@ -1533,6 +1742,10 @@ "message": "बेस डोमेन", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "डोमेन नाम", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Match Detection", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "डिफॉल्ट मैच डिटेक्शन", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Toggle Options" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "सूची के लिए कोई पासवर्ड नहीं हैं।" }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "हटाएं" }, @@ -1677,6 +1899,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": "वॉल्ट मध्यांतर कार्रवाई" }, @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "आपका नया मास्टर पासवर्ड पॉलिसी आवश्यकताओं को पूरा नहीं करता है।" }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,12 @@ "nativeMessagingWrongUserTitle": { "message": "खाता गलत मैच" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "बायोमेट्रिक अनलॉक विफल रहा. बायोमेट्रिक गुप्त कुंजी वॉल्ट को अनलॉक करने में विफल रही. कृपया बायोमेट्रिक्स को फिर से सेट करने का प्रयास करें." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "बेमेल बायोमेट्रिक कुंजी" + }, "biometricsNotEnabledTitle": { "message": "बॉयोमीट्रिक्स सक्षम नहीं है" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "बहिष्कृत डोमेन" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ is not a valid domain", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "भेजें", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "पासवर्ड सुरक्षित है" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "कॉपी Send लिंक", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "नया सेंड बनाया गया", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "सेंड एडिट किया गया", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "ईमेल सत्यापन आवश्यक है" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "इस सुविधा का उपयोग करने के लिए आपको अपने ईमेल को सत्यापित करना होगा। आप वेब वॉल्ट में अपने ईमेल को सत्यापित कर सकते हैं।" }, @@ -2179,6 +2476,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": "आपके संगठन ने विश्वसनीय डिवाइस एन्क्रिप्शन अक्षम कर दिया है. कृपया अपने वॉल्ट तक पहुँचने के लिए मास्टर पासवर्ड सेट करें." + }, "resetPasswordPolicyAutoEnroll": { "message": "स्वचालित नामांकन" }, @@ -2606,7 +2906,7 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { "message": "ऑटो-फ़िल कैसे करें" @@ -2627,16 +2927,25 @@ "message": "Got it" }, "autofillSettings": { - "message": "Auto-fill settings" + "message": "Autofill settings" + }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "डोमेन उपनाम" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "सीधे सामग्री पर जाएं" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "फ़िल्टर" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "बिटवार्डन का नया रूप!" + }, + "bitwardenNewLookDesc": { + "message": "वॉल्ट टैब से ऑटोफिल और सर्च करना पहले से कहीं ज़्यादा आसान और सहज है। सबकुछ ध्यान से देखें!" + }, + "accountActions": { + "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Show number of login autofill suggestions on extension icon" + }, + "systemDefault": { + "message": "System default" + }, + "enterprisePolicyRequirementsApplied": { + "message": "इस सेटिंग पर एंटरप्राइज़ नीति आवश्यकताएँ लागू की गई हैं" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 968ec93fcfa..88a1f158b3a 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -3,27 +3,27 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden upravitelj lozinki", "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": "Kod kuće, na poslu ili u pokretu, Bitwarden štiti sve tvoje lozinke, pristupne ključeve i osjetljive informacije", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Prijavi se ili stvori novi račun za pristup svojem sigurnom trezoru." }, + "inviteAccepted": { + "message": "Pozivnica prihvaćena" + }, "createAccount": { "message": "Stvori račun" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Postavi jaku lozinku" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" - }, - "login": { - "message": "Prijava" + "message": "Dovrši stvaranje svog računa postavljanjem lozinke" }, "enterpriseSingleSignOn": { "message": "Jedinstvena prijava na razini tvrtke (SSO)" @@ -50,7 +50,7 @@ "message": "Podsjetnik glavne lozinke ti može pomoći da se prisjetiš svoje lozinke ako ju zaboraviš." }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Podsjetnik ti možemo poslati ako zaboraviš svoju lozinku. Najviše $CURRENT$/$MAXIMUM$ znakova.", "placeholders": { "current": { "content": "$1", @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Podsjetnik glavne lozinke (neobavezno)" }, + "joinOrganization": { + "message": "Pridruži se organizaciji" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Dovrši pridruživanje organizaciji postavljanjem glavne lozinke." + }, "tab": { "message": "Kartica" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Kopiraj kontrolni broj" }, + "copyName": { + "message": "Kopiraj naziv" + }, + "copyCompany": { + "message": "Kopiraj tvrtku" + }, + "copySSN": { + "message": "Kopiraj OIB" + }, + "copyPassportNumber": { + "message": "Kopiraj broj putovnice" + }, + "copyLicenseNumber": { + "message": "Kopiraj OIB" + }, "autoFill": { "message": "Auto-ispuna" }, @@ -189,25 +210,25 @@ "message": "Promjeni glavnu lozinku" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Nastavi na web aplikaciju?" }, "continueToWebAppDesc": { - "message": "Explore more features of your Bitwarden account on the web app." + "message": "Pronađi viđe značajki svojeg Bitwarden računa u web aplikaciji." }, "continueToHelpCenter": { - "message": "Continue to Help Center?" + "message": "Nastavi u centar za pomoć?" }, "continueToHelpCenterDesc": { - "message": "Learn more about how to use Bitwarden on the Help Center." + "message": "Za pomoć oko korištenja Bitwardena posjeti centar za pomoć." }, "continueToBrowserExtensionStore": { - "message": "Continue to browser extension store?" + "message": "Nastaviti na trgovinu proširenja za preglednik?" }, "continueToBrowserExtensionStoreDesc": { - "message": "Help others find out if Bitwarden is right for them. Visit your browser's extension store and leave a rating now." + "message": "Želiš preporučiti Bitwarden drugima? Posjeti trgovinu proširenja svojeg preglednika i ostavi recenziju." }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Svoju lozinku možeš promijeniti u Bitwarden web aplikaciji." }, "fingerprintPhrase": { "message": "Jedinstvena fraza", @@ -224,43 +245,43 @@ "message": "Odjava" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "O Bitwardenu" }, "about": { "message": "O aplikaciji" }, "moreFromBitwarden": { - "message": "More from Bitwarden" + "message": "Više od Bitwardena" }, "continueToBitwardenDotCom": { - "message": "Continue to bitwarden.com?" + "message": "Nastavi na bitwarden.com?" }, "bitwardenForBusiness": { - "message": "Bitwarden for Business" + "message": "Bitwarden za tvrtke" }, "bitwardenAuthenticator": { - "message": "Bitwarden Authenticator" + "message": "Bitwarden autentifikator" }, "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": "Bitwarden autentifikator omogućuje pohranu ključeva za autentifikaciju i generiranje TOTP kodova za dvostruku autentifikaciju. Saznaj više na web stranici bitwarden.com" }, "bitwardenSecretsManager": { "message": "Bitwarden Secrets Manager" }, "continueToSecretsManagerPageDesc": { - "message": "Securely store, manage, and share developer secrets with Bitwarden Secrets Manager. Learn more on the bitwarden.com website." + "message": "Sigurno pohrani, upravljaj i dijeli programerske tajne s Bitwarden Secrets Managerom. Saznajte više na web stranici 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": "Stvori laka i sigurna iskustva prijave bez tradicionalnih lozinki uz Passwordless.dev. Saznaj više na web stranici bitwarden.com." }, "freeBitwardenFamilies": { - "message": "Free Bitwarden Families" + "message": "Besplatan obiteljski Bitwarden" }, "freeBitwardenFamiliesPageDesc": { - "message": "You are eligible for Free Bitwarden Families. Redeem this offer today in the web app." + "message": "Ispunjavaš uvjete za besplatni obiteljski Bitwarden. Iskoristi ovu ponudu u web aplikaciji već danas." }, "version": { "message": "Verzija" @@ -280,6 +301,24 @@ "editFolder": { "message": "Uredi mapu" }, + "newFolder": { + "message": "Nova mapa" + }, + "folderName": { + "message": "Naziv mape" + }, + "folderHintText": { + "message": "Ugnijezdi mapu dodavanjem naziva roditeljske mape i znaka kroz. Npr. Mreže/Forumi" + }, + "noFoldersAdded": { + "message": "Mapa nije dodana" + }, + "createFoldersToOrganize": { + "message": "Za organiziranje stavki u trezoru, stvori mape" + }, + "deleteFolderPermanently": { + "message": "Sigurno želiš trajno izbrisati ovu mapu?" + }, "deleteFolder": { "message": "Izbriši mapu" }, @@ -321,7 +360,7 @@ "message": "Automatski generiraj jake, jedinstvene lozinke." }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "Bitwarden web trezor" }, "importItems": { "message": "Uvoz stavki" @@ -336,7 +375,7 @@ "message": "Ponovno generiraj lozinku" }, "options": { - "message": "Opcije" + "message": "Mogućnosti" }, "length": { "message": "Duljina" @@ -345,16 +384,56 @@ "message": "Minimalna duljina lozinke" }, "uppercase": { - "message": "Velika slova (A - Z)" + "message": "Velika slova (A - Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Mala slova (a - z)" + "message": "Mala slova (a - z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Brojevi (0 - 9)" + "message": "Brojevi (0 - 9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Posebni znakovi (!@#$%^&*)" + "message": "Posebni znakovi (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Uključi", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Uključi velika slova", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Uključi mala slova", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Uključi brojeve", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Uključi posebne znakove", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Broj riječi" @@ -376,7 +455,12 @@ "message": "Najmanje posebnih" }, "avoidAmbChar": { - "message": "Izbjegavaj dvosmislene znakove" + "message": "Izbjegavaj dvosmislene znakove", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Izbjegavaj dvosmislene znakove", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Pretraži trezor" @@ -409,13 +493,13 @@ "message": "Favorit" }, "unfavorite": { - "message": "Unfavorite" + "message": "Ukloni iz favorita" }, "itemAddedToFavorites": { - "message": "Item added to favorites" + "message": "Dodaj stavku u omiljene" }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "Stavka uklonjenja iz omiljenih" }, "notes": { "message": "Bilješke" @@ -439,7 +523,7 @@ "message": "Pokreni" }, "launchWebsite": { - "message": "Launch website" + "message": "Pokreni web stranicu" }, "website": { "message": "Web stranica" @@ -454,7 +538,7 @@ "message": "Ostalo" }, "unlockMethods": { - "message": "Unlock options" + "message": "Mogućnosti otključavanja" }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Za promjenu vremena isteka trezora, odredi način otključavanja." @@ -463,10 +547,10 @@ "message": "Postavi način otključavanja u Postavkama" }, "sessionTimeoutHeader": { - "message": "Session timeout" + "message": "Istek sesije" }, "otherOptions": { - "message": "Other options" + "message": "Ostale postavke" }, "rateExtension": { "message": "Ocijeni proširenje" @@ -556,6 +640,18 @@ "security": { "message": "Sigurnost" }, + "confirmMasterPassword": { + "message": "Potvrdi glavnu lozinku" + }, + "masterPassword": { + "message": "Glavna lozinka" + }, + "masterPassImportant": { + "message": "Glavnu lozinku nije moguće oporaviti ako ju zaboraviš!" + }, + "masterPassHintLabel": { + "message": "Podsjetnik glavne lozinke" + }, "errorOccurred": { "message": "Došlo je do pogreške" }, @@ -585,13 +681,19 @@ "message": "Potvrda glavne lozinke se ne podudara." }, "newAccountCreated": { - "message": "Tvoj novi račun je kreiran! Sada se možeš prijaviti." + "message": "Tvoj novi račun je stvoren! Sada se možeš prijaviti." + }, + "newAccountCreated2": { + "message": "Tvoj novi račun je stvoren!" + }, + "youHaveBeenLoggedIn": { + "message": "Prijava uspješna!" }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Prijava uspješna" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Možeš zatvoriti ovaj prozor" }, "masterPassSent": { "message": "Poslali smo e-poštu s podsjetnikom glavne lozinke." @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Potvrdni kôd je obavezan." }, + "webauthnCancelOrTimeout": { + "message": "Autentifikacija je otkazana ili je trajala predugo. Molimo pokušaj ponovno." + }, "invalidVerificationCode": { "message": "Nevažeći kôd za provjeru" }, @@ -622,7 +727,19 @@ "message": "Ključ autentifikatora je dodan" }, "totpCapture": { - "message": "Skenirajte QR kod autentifikatora s trenutne web stranice" + "message": "Skeniraj QR kôd autentifikatora s trenutne web stranice" + }, + "totpHelperTitle": { + "message": "Učini dvostruku autentifikaciju besprijekornom" + }, + "totpHelper": { + "message": "Bitwarden može pohraniti i ispuniti kodove za dvostruku autentifikaciju. Kopiraj i zalijepi ključ u ovo polje." + }, + "totpHelperWithCapture": { + "message": "Bitwarden može pohraniti i ispuniti kodove za dvostruku autentifikaciju. Odaberi ikonu kamere i označi QR kôd za provjeru autentičnosti ove web stranice ili kopiraj i zalijepi ključ u ovo polje." + }, + "learnMoreAboutAuthenticators": { + "message": "Više o autentifikatorima" }, "copyTOTP": { "message": "Kopiraj ključ autentifikatora (TOTP)" @@ -631,11 +748,26 @@ "message": "Odjavljen" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "Odjavljen/a si sa svog računa." }, "loginExpired": { "message": "Sesija je istekla." }, + "logIn": { + "message": "Prijavi se" + }, + "restartRegistration": { + "message": "Ponovno pokreni registraciju" + }, + "expiredLink": { + "message": "Istekla poveznica" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Ponovno pokreni registraciju ili se pokušaj prijaviti." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Možda već imaš račun" + }, "logOutConfirmation": { "message": "Sigurno se želiš odjaviti?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Novi URI" }, + "addDomain": { + "message": "Dodaj domenu", + "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": { "message": "Stavka dodana" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Upitaj za dodavanje prijave" }, + "vaultSaveOptionsTitle": { + "message": "Mogućnosti spremanja u trezor" + }, "addLoginNotificationDesc": { "message": "Upit za dodavanje prijave pojavljuje se kada se otkrije prva prijava na neko web mjesto. Bitwarden će te pitatati želiš li uneseno korisničko ime i lozinku spremiti u svoj trezor." }, "addLoginNotificationDescAlt": { "message": "Pitaj za dodavanje stavke ako nije pronađena u tvojem trezoru. Primjenjuje se na sve prijavljene račune." }, + "showCardsInVaultView": { + "message": "Prikaži kartice kao prijedloge za auto-ispunu u prikazu trezora" + }, "showCardsCurrentTab": { "message": "Prikaži platne kartice" }, "showCardsCurrentTabDesc": { "message": "Prikazuj platne kartice za jednostavnu auto-ispunu." }, + "showIdentitiesInVaultView": { + "message": "Prikaži identitete kao prijedloge za auto-ispunu u prikazu trezora" + }, "showIdentitiesCurrentTab": { "message": "Prikaži identitete" }, @@ -779,7 +924,7 @@ "message": "Pitaj za ažuriranje lozinke za prijavu kada se otkrije promjena na web stranici. Primjenjuje se na sve prijavljene račune." }, "enableUsePasskeys": { - "message": "Pitaj za spremanje i korištenje pristupnih ključeva" + "message": "Pitaj za spremanje/korištenje pristupnih ključeva" }, "usePasskeysDesc": { "message": "Pitaj za spremanje novih pristupnih ključeva ili se prijavi pomoću pristupnih ključeva pohranjenih u tvojem trezoru. Primjenjuje se na sve prijavljene račune." @@ -791,13 +936,13 @@ "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" }, "additionalOptions": { - "message": "Additional options" + "message": "Dodatne mogućnosti" }, "enableContextMenuItem": { "message": "Prikaži opcije kotekstualnog izbornika" @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Zadano otkrivanje URI podudaranja", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Odaberi zadani način na koji će se riješavati otkrivanje URI-ja za prijavu pri izvođenju radnji kao što je auto-ispuna." @@ -837,7 +982,7 @@ "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, "exportFrom": { - "message": "Export from" + "message": "Izvezi iz" }, "exportVault": { "message": "Izvezi trezor" @@ -846,28 +991,28 @@ "message": "Format datoteke" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "Ova izvozna datoteka biti će zaštićena lozinkom bez koje ju neće biti moguće dešifrirati." }, "filePassword": { - "message": "File password" + "message": "Lozinka datoteke" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "Ova će se lozinka koristiti za izvoz i uvoz ove datoteke" }, "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": "Upotrijebi svoj ključ za šifriranje računa, izveden iz korisničkog imena i glavne lozinke za šifriranje izvoza i ograničavanje uvoza samo na trenutni Bitwarden račun." }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "Bitwarden omogućuje dijeljenje trezora s drugima pomoću organizacijskog računa. Za više informacija posjeti bitwarden. com." }, "exportTypeHeading": { - "message": "Export type" + "message": "Tip izvoza" }, "accountRestricted": { - "message": "Account restricted" + "message": "Račun ograničen" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "Lozinka se ne podudara." }, "warning": { "message": "UPOZORENJE", @@ -892,7 +1037,7 @@ "message": "Dijeljeno" }, "bitwardenForBusinessPageDesc": { - "message": "Bitwarden for Business allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website." + "message": "Bitwarden for Business omogućuje dijeljenje stavki trezora s drugima koristeći organizacije. Za više informacija posjeti bitwarden.com." }, "moveToOrganization": { "message": "Premjesti u organizaciju" @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB šifriranog prostora za pohranu podataka." }, + "premiumSignUpEmergency": { + "message": "Pristup u nuždi." + }, "premiumSignUpTwoStepOptions": { "message": "Mogućnosti za prijavu u dva koraka kao što su YubiKey i Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Možeš kupiti premium članstvo na web trezoru. Želiš li sada posjetiti bitwarden.com?" }, + "premiumPurchaseAlertV2": { + "message": "Premium možeš kupiti u postavkama računa na Bitwarden web aplikaciji." + }, "premiumCurrentMember": { "message": "Ti si premium član!" }, "premiumCurrentMemberThanks": { "message": "Hvala ti što podupireš Bitwarden." }, + "premiumFeatures": { + "message": "Nadogradi na Premium za:" + }, "premiumPrice": { "message": "Sve za samo $PRICE$ /godišnje!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "Sve samo za $PRICE$ /godišnje!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Osvježavanje završeno" }, @@ -1028,7 +1191,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" @@ -1106,17 +1269,17 @@ "message": "Autentifikatorska aplikacija" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Unesi kôd generiran autentifikatorskom aplikacijom kao npr. Bitwarden Authenticatorom.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP Security Key" + "message": "Yubico OTP sigurnosni ključ" }, "yubiKeyDesc": { "message": "Koristi YubiKey za pristup svojem računu. Radi s YubiKey 4, 4 Nano, 4C i NEO uređajima." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Unesi kôd generiran Duo Securityjem.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1133,7 +1296,7 @@ "message": "E-pošta" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Unesi kôd poslan e-poštom." }, "selfHostedEnvironment": { "message": "Vlastito hosting okruženje" @@ -1142,13 +1305,13 @@ "message": "Navedi osnovni URL svoje lokalno smještene Bitwarden instalacije." }, "selfHostedBaseUrlHint": { - "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" + "message": "Navedi osnovni URL svoje lokalne Bitwarden instalacije, npr.: https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { - "message": "For advanced configuration, you can specify the base URL of each service independently." + "message": "Kao naprednu postavku, možeš odrediti osnovni URL svake usluge zasebno." }, "selfHostedEnvFormInvalid": { - "message": "You must add either the base Server URL or at least one custom environment." + "message": "Moraš dodati ili osnovni URL poslužitelja ili barem jedno prilagođeno okruženje." }, "customEnvironment": { "message": "Prilagođeno okruženje" @@ -1179,13 +1342,22 @@ }, "showAutoFillMenuOnFormFields": { "message": "Prikaži izbornik za auto-ispunu u poljima obrasca", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Prijedlozi auto-ispune" + }, + "showInlineMenuLabel": { + "message": "Prikaži prijedloge auto-ispune na poljima obrazaca" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Prikaži prijedloge kada je odabrana ikona" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Primjenjuje se na sve prijavljene račune." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Isključite postavke upravitelja zaporki ugrađene u preglednik kako biste izbjegli sukobe." + "message": "Isključi preglednikov upravitelj lozinki kako bi izbjegli sukobe." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Uredi postavke preglednika." @@ -1202,15 +1374,34 @@ "message": "Kada je odabrana ikona auto-ispune", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Auto-ispuna kod učitavanja stranice" + }, "enableAutoFillOnPageLoad": { "message": "Auto-ispuna kod učitavanja" }, "enableAutoFillOnPageLoadDesc": { "message": "Nakon učitavanja web stranice, ako je otkriven obrazac za prijavu, auto-ispuni." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Upozorenje:$CLOSETAG$ Ugrožene ili nepouzdane web stranice mogu iskoristiti auto-ispunu prilikom učitavanja stranice.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Ugrožene ili nepouzdane web stranice mogu iskoristiti auto-ispunu prilikom učitavanja stranice." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Saznaj više o rizicima" + }, "learnMoreAboutAutofill": { "message": "Saznaj više o auto-ispuni" }, @@ -1218,7 +1409,7 @@ "message": "Zadana postvaka Auto-ispune za prijave" }, "defaultAutoFillOnPageLoadDesc": { - "message": "Nakon omogućavanja auto-ispune kod učitavanja stranice, moguće je uključiti/isključiti ovu značajku za svaku pojedinu prijavu. Ovo je zadana postavka za prijave koje nisu pojedinčano određene." + "message": "Auto-ispunu kod učitavanju stranice je moguće uključiti/isključiti za svaku pojedinu prijavu unutar uređivanja stavke." }, "itemAutoFillOnPageLoad": { "message": "Auto-ispuna kod učitavanja stranice (ako je uključeno u Postavkama)" @@ -1227,10 +1418,10 @@ "message": "Koristi zadane postavke" }, "autoFillOnPageLoadYes": { - "message": "Auto-ispuna kod učitavanja" + "message": "Auto-ispuna kod učitavanja stranice" }, "autoFillOnPageLoadNo": { - "message": "Ne koristi Auto-ispunu kod učitavanja" + "message": "Ne koristi auto-ispunu kod učitavanja stranice" }, "commandOpenPopup": { "message": "Otvori iskočni prozor trezora" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Otvori trezor u bočnoj traci" }, - "commandAutofillDesc": { - "message": "Auto-ispuni zadnju korištenu prijavu za trenutnu web stranicu." + "commandAutofillLoginDesc": { + "message": "Auto-ispuni zadnje korištenu prijavu za trenutnu web stranicu" + }, + "commandAutofillCardDesc": { + "message": "Auto-ispuni zadnje korištenu karticu za trenutnu web stranicu" + }, + "commandAutofillIdentityDesc": { + "message": "Auto-ispuni zadnje korišteni identitet za trenutnu web stranicu" }, "commandGeneratePasswordDesc": { "message": "Generiraj i kopiraj novu nasumičnu lozinku u međuspremnik." @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Potvrdni okvir" + }, "cfTypeLinked": { "message": "Povezano", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1376,7 +1576,7 @@ "message": "dr." }, "mx": { - "message": "Mx" + "message": "gx." }, "firstName": { "message": "Ime" @@ -1397,13 +1597,13 @@ "message": "Tvrtka" }, "ssn": { - "message": "Broj zdravstvenog osiguranja" + "message": "OIB" }, "passportNumber": { "message": "Broj putovnice" }, "licenseNumber": { - "message": "Broj vozačke dozvole" + "message": "Broj osobne iskaznice" }, "email": { "message": "E-pošta" @@ -1454,7 +1654,7 @@ "message": "Identitet" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "Novi $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1463,7 +1663,16 @@ } }, "editItemHeader": { - "message": "Edit $TYPE$", + "message": "Uredi $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, + "viewItemHeader": { + "message": "Pogledaj $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1481,7 +1690,7 @@ "message": "Zbirke" }, "nCollections": { - "message": "$COUNT$ collections", + "message": "$COUNT$ zbirki", "placeholders": { "count": { "content": "$1", @@ -1533,6 +1742,10 @@ "message": "Primarna domena", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Osnovna domena (preporučeno)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Naziv domene", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Otkrivanje podudaranja", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Zadano otkrivanje podudaranja", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Uključi/isključi opcije" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Nema lozinki na popisu." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Ukloni" }, @@ -1651,7 +1873,7 @@ "message": "Nerispravan PIN." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "Previše netočnih pokušaja unosa PIN-a. Odjava..." }, "unlockWithBiometrics": { "message": "Otključaj biometrijom" @@ -1677,6 +1899,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Jedno ili više pravila organizacije utječe na postavke generatora." }, + "passwordGenerator": { + "message": "Generator lozinki" + }, + "usernameGenerator": { + "message": "Generator korisničkih imena" + }, + "useThisPassword": { + "message": "Koristi ovu lozinku" + }, + "useThisUsername": { + "message": "Koristi ovo korisničko ime" + }, + "securePasswordGenerated": { + "message": "Sigurna lozinka generirana! Ne zaboravi ažurirati lozinku na web stranici." + }, + "useGeneratorHelpTextPartOne": { + "message": "Koristi 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": "za stvaranje snažne, jedinstvene lozinke", + "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" }, @@ -1695,7 +1940,7 @@ "message": "Trajno izbriši stavku" }, "permanentlyDeleteItemConfirmation": { - "message": "Želiš li zaista trajno izbrisati ovu stavku?" + "message": "Sigurno želiš trajno izbrisati ovu stavku?" }, "permanentlyDeletedItem": { "message": "Stavka trajno izbrisana" @@ -1707,7 +1952,7 @@ "message": "Stavka vraćena" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Već imaš račun?" }, "vaultTimeoutLogOutConfirmation": { "message": "Odjava će ukloniti pristup tvom trezoru i zahtijevati mrežnu potvrdu identiteta nakon isteka vremenske neaktivnosti. Sigurno želiš koristiti ovu postavku?" @@ -1719,13 +1964,13 @@ "message": "Auto-ispuni i spremi" }, "fillAndSave": { - "message": "Fill and save" + "message": "Ispuni i spremi" }, "autoFillSuccessAndSavedUri": { - "message": "Auto-ispunjena stavka i spremanje URI" + "message": "Stavka auto-ispunjena i spremljen URI" }, "autoFillSuccess": { - "message": "Auto-ispunjena stavka" + "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." @@ -1799,20 +2044,20 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Tvoja nova glavna lozinka ne ispunjava zahtjeve." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Primaj e-poštom od Bitwardena savjete, najave i mogućnosti istraživanja." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "Poništi pretplatu" }, "atAnyTime": { - "message": "at any time." + "message": "bilo kada." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Ako nastaviš, slažeš se s" }, "and": { - "message": "and" + "message": "i" }, "acceptPolicies": { "message": "Označavanjem ove kućice slažete se sa sljedećim:" @@ -1833,10 +2078,10 @@ "message": "U redu" }, "errorRefreshingAccessToken": { - "message": "Access Token Refresh Error" + "message": "Pogreška osvježavanja tokena pristupa" }, "errorRefreshingAccessTokenDesc": { - "message": "No refresh token or API keys found. Please try logging out and logging back in." + "message": "Nije pronađen token za osvježavanje ili API ključevi. Pokušaj se odjaviti i ponovno prijaviti." }, "desktopSyncVerificationTitle": { "message": "Potvrda desktop sinkronizacije" @@ -1874,6 +2119,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" }, @@ -1887,10 +2138,16 @@ "message": "Biometrija preglednika nije podržana na ovom uređaju." }, "biometricsNotUnlockedTitle": { - "message": "User locked or logged out" + "message": "Korisnik zaključan ili odjavljen" }, "biometricsNotUnlockedDesc": { - "message": "Please unlock this user in the desktop application and try again." + "message": "Otključaj ovog korisnika u desktop aplikaciji i pokušaj ponovno." + }, + "biometricsNotAvailableTitle": { + "message": "Biometrijsko otključavanje nije dostupno" + }, + "biometricsNotAvailableDesc": { + "message": "Biometrijsko otključavanje trenutno nije dostupno. Pokušaj ponovno kasnije." }, "biometricsFailedTitle": { "message": "Biometrija neuspješna" @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "Organizacijsko pravilo onemogućuje uvoz stavki u tvoj osobni trezor." }, + "domainsTitle": { + "message": "Domene", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Izuzete domene" }, @@ -1926,17 +2187,29 @@ "message": "Bitwarden neće pitati treba li spremiti prijavne podatke za ove domene. Za primjenu promjena, potrebno je osvježiti stranicu." }, "excludedDomainsDescAlt": { - "message": "Bitwarden neće tražiti spremanje podataka za prijavu za ove domene za sve prijavljene račune. Moraš osvježiti stranicu kako bi promjene stupile na snagu." + "message": "Bitwarden neće nuditi spremanje podataka za prijavu za ove domene za sve prijavljene račune. Moraš osvježiti stranicu kako bi promjene stupile na snagu." + }, + "websiteItemLabel": { + "message": "Web stranica $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ nije valjana domena", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Spremljene promjene izuzete domene" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Zaštićeno lozinkom" }, + "copyLink": { + "message": "Kopiraj vezu" + }, "copySendLink": { "message": "Kopiraj vezu na Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send stvoren", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send je uspješno stvoren!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "Send će biti dostupan svakome s poveznicom ovoliko dana: $DAYS$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Kopirana poveznica Senda", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send spremljen", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Potrebna je potvrda e-pošte" }, + "emailVerifiedV2": { + "message": "e-pošta potvrđena" + }, "emailVerificationRequiredDesc": { "message": "Moraš ovjeriti svoju e-poštu u mrežnom trezoru za koritšenje ove značajke." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatsko učlanjenje" }, @@ -2189,7 +2489,7 @@ "message": "Odaberi mapu..." }, "noFoldersFound": { - "message": "No folders found", + "message": "Nema pronađenih mapa", "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { @@ -2201,7 +2501,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { - "message": "Verification required", + "message": "Potrebna je potvrda", "description": "Default title for the user verification dialog." }, "hours": { @@ -2307,10 +2607,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "Izvoz organizacijskog trezora" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Izvest će se samo organizacijski trezor povezan s $ORGANIZATION$. Stavke iz osobnih trezora i stavke iz drugih organizacija neće biti uključene.", "placeholders": { "organization": { "content": "$1", @@ -2368,7 +2668,7 @@ "message": "Generiraj pseudonim e-pošte s vanjskom uslugom prosljeđivanja." }, "forwarderError": { - "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "message": "$SERVICENAME$ greška: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", "placeholders": { "servicename": { @@ -2382,11 +2682,11 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "Generirao Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "Web: $WEBSITE$. Generirao Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -2396,7 +2696,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "Nevažeći $SERVICENAME$ API token", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -2406,7 +2706,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "Nevažeći $SERVICENAME$ API token: $ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -2420,7 +2720,7 @@ } }, "forwarderNoAccountId": { - "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "message": "Nije moguće dobiti $SERVICENAME$ maskirani ID računa e-pošte.", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -2430,7 +2730,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "Nevažeća $SERVICENAME$ domena.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -2440,7 +2740,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "Nevažeći $SERVICENAME$ URL.", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -2450,7 +2750,7 @@ } }, "forwarderUnknownError": { - "message": "Unknown $SERVICENAME$ error occurred.", + "message": "Nepoznata $SERVICENAME$ greška.", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -2460,7 +2760,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Unknown forwarder: '$SERVICENAME$'.", + "message": "Nepoznati prosljeditelj: '$SERVICENAME$.", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Postavke auto-ispune" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Prečac auto-ispune" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Promijeni prečac" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Upravljaj prečacima" + }, "autofillShortcut": { "message": "Tipkovnički precač auto-ispune" }, - "autofillShortcutNotSet": { - "message": "Prečac auto-ispune nije postavljen. Promijeni u postavkama preglednika." + "autofillLoginShortcutNotSet": { + "message": "Prečac auto-ispune prijave nije postavljen. Promijeni u postavkama preglednika." }, - "autofillShortcutText": { - "message": "Prečac auto-ispune je: $COMMAND$. Promijeni u postavkama preglednika.", + "autofillLoginShortcutText": { + "message": "Prečac auto-ispune prijave je: $COMMAND$. Promijeni u postavkama preglednika.", "placeholders": { "command": { "content": "$1", @@ -2681,25 +2990,25 @@ "message": "Potreban je identifikator organizacije." }, "creatingAccountOn": { - "message": "Creating account on" + "message": "Stvaranje računa na" }, "checkYourEmail": { - "message": "Check your email" + "message": "Provjeri svoju e-poštu" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "Slijedi vezu u e-pošti poslanoj na" }, "andContinueCreatingYourAccount": { - "message": "and continue creating your account." + "message": "za nastavak stvaranja tvojeg računa." }, "noEmail": { - "message": "No email?" + "message": "Nema e-pošte?" }, "goBack": { - "message": "Go back" + "message": "Nazad" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "na uređivanje svoje adrese e-pošte." }, "eu": { "message": "EU", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Uređaj pouzdan" }, + "sendsNoItemsTitle": { + "message": "Nema aktivnih Sendova", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Koristi Send za sigurno slanje šifriranih podataka bilo kome.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Potreban je unos." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 polje treba tvoju pažnju." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ polja treba tvoju pažnju.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Odaberi --" }, @@ -2843,11 +3172,11 @@ "description": "Toggling an expand/collapse state." }, "filelessImport": { - "message": "Uvesti svoje podatke u Bitwarden?", + "message": "Uvezi svoje podatke u Bitwarden?", "description": "Default notification title for triggering a fileless import." }, "lpFilelessImport": { - "message": "Zaštititi svoje LastPass podatke i uvesti ih u Bitwarden?", + "message": "Zaštititi svoje LastPass podatke i uvezi ih u Bitwarden?", "description": "LastPass specific notification title for triggering a fileless import." }, "lpCancelFilelessImport": { @@ -2859,15 +3188,15 @@ "description": "Notification button text for starting a fileless import." }, "importing": { - "message": "Uvoženje...", + "message": "Uvoz...", "description": "Notification message for when an import is in progress." }, "dataSuccessfullyImported": { - "message": "Podaci su uspješno uvezeni!", + "message": "Uvoz podataka u trezor uspješan!", "description": "Notification message for when an import has completed successfully." }, "dataImportFailed": { - "message": "Greška pri uvozu. Provjerite konzolu za detalje.", + "message": "Greška pri uvozu. Provjeri konzolu za detalje.", "description": "Notification message for when an import has failed." }, "importNetworkError": { @@ -2879,21 +3208,21 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Stavke za koje je potrebna glavna lozinka neće se auto-ispuniti kod učitavanja stranice. Auto-ispuna pri učitavanju stranice je isključena.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "Auto-ispuna kod učitavanja stranice koristi zadane postavke.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Isključi traženje glavne lozinke za promjenu ovog polja", "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "U/Isključi bočnu navigaciju" }, "skipToContent": { - "message": "Skoči na sadržaj" + "message": "Preskoči na sadržaj" }, "bitwardenOverlayButton": { "message": "Tipka izbornika Bitwarden auto-ispune", @@ -2911,10 +3240,18 @@ "message": "Otklučaj svoj račun za prikaz podudarnih prijava", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Otključaj račun za prikaz prijedloga auto-ispuna", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Otključaj račun", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Otključaj račun; otvara se u novom prozoru", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Unesi vjerodajnice za", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Dodaj novu stavku trezora", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "Nova prijava", + "description": "Button text to display within inline menu when there are no matching items on a login field" + }, + "addNewLoginItemAria": { + "message": "Dodaj novu stavku prijave u trezor; otvara se u novom prozoru", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "Nova kartica", + "description": "Button text to display within inline menu when there are no matching items on a credit card field" + }, + "addNewCardItemAria": { + "message": "Dodaj novu stavku kartice u trezor; otvara se u novom prozoru", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "Novi identitet", + "description": "Button text to display within inline menu when there are no matching items on an identity field" + }, + "addNewIdentityItemAria": { + "message": "Dodaj novu stavku identiteta u trezor; otvara se u novom prozoru", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Dostupan je Bitwarden izbornik auto-ispune. Pritisni tipku sa strelicom prema dolje za odabir.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -2974,40 +3335,40 @@ } }, "tryAgain": { - "message": "Try again" + "message": "Pokušaj ponovno" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "Za ovu radnju potrebna je potvrda. Postavi PIN za nastavak." }, "setPin": { - "message": "Set PIN" + "message": "Postavi PIN" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Potvrdi biometrijom" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Čekanje potvrde" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "Nije moguće dovršiti biometriju." }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "Koristi drugi način?" }, "useMasterPassword": { - "message": "Use master password" + "message": "Koristi glavnu lozinku" }, "usePin": { - "message": "Use PIN" + "message": "Koristi PIN" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Koristi biometriju" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "Unesi kôd za potvrdu primljen e-poštom." }, "resendCode": { - "message": "Resend code" + "message": "Ponovno pošalji kod" }, "total": { "message": "Ukupno" @@ -3021,20 +3382,23 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Greška pri povezivanju s uslugom Duo. Koristi drugu metodu prijave s dvostrukom autentifikacijom ili kontaktiraj Duo za pomoć." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "Pokreni Duo i slijedi korake za dovršetak prijave." }, "duoRequiredForAccount": { - "message": "Duo two-step login is required for your account." + "message": "Za tvoj račun je potrebna Duo dvostruka autentifikacija." }, "popoutTheExtensionToCompleteLogin": { - "message": "Popout the extension to complete login." + "message": "Otvori proširenje za dovršetak prijave." }, "popoutExtension": { - "message": "Popout extension" + "message": "Otvori proširenje" }, "launchDuo": { - "message": "Launch Duo" + "message": "Pokreni Duo" }, "importFormatError": { "message": "Podaci nisu ispravno formatirani. Provjeri uvoznu datoteku i pokušaj ponovno." @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Nesipravna lozinka datoteke. Unesi lozinku izvozne datoteke." }, - "importDestination": { - "message": "Odredište uvoza" + "destination": { + "message": "Odredište" }, "learnAboutImportOptions": { "message": "Više o mogućnostima uvoza" @@ -3108,7 +3472,7 @@ "message": "Potvrdi lozinku datoteke" }, "exportSuccess": { - "message": "Vault data exported" + "message": "Podaci iz trezora su izvezeni" }, "typePasskey": { "message": "Pristupni ključ" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Ishodišna stranica zahtijeva verifikaciju. Ova značajka još nije implementirana za račune bez glavne lozinke." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Prijava pristupnim ključem?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "Nema odgovarajuće prijavu za ovu stranicu." }, + "noMatchingLoginsForSite": { + "message": "Nema prijava za ovu web stranicu" + }, "confirm": { "message": "Autoriziraj" }, @@ -3143,8 +3510,11 @@ "savePasskeyNewLogin": { "message": "Spremi pristupni ključ kao novu prijavu" }, - "choosePasskey": { - "message": "Odaberi prijavu za spremanje ovog pristupnog ključa" + "chooseCipherForPasskeySave": { + "message": "Odaberi za koju prijavu želiš spremiti ovaj pristupni ključ" + }, + "chooseCipherForPasskeyAuth": { + "message": "Odaberi pristupni ključ za prijavu" }, "passkeyItem": { "message": "Stavka pristupnog ključa" @@ -3171,13 +3541,13 @@ "message": "Neispravno korisničko ime ili lozinka" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "Neispravna lozinka" }, "incorrectCode": { - "message": "Incorrect code" + "message": "Neispravan kôd" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "Neispravan PIN" }, "multifactorAuthenticationFailed": { "message": "Multifaktorska autentifikacija nije uspjela" @@ -3201,7 +3571,7 @@ "message": "Odobri svoj zahtjev za prijavu u svojoj aplikaciji za autentifikaciju ili unesi jednokratni kôd." }, "passcode": { - "message": "Šifra" + "message": "Jednokratni kôd" }, "lastPassMasterPassword": { "message": "LastPass glavna lozinka" @@ -3250,25 +3620,25 @@ "message": "Dostupni računi" }, "accountLimitReached": { - "message": "Dosegnuto je ograničenje računa. Odjavite se s računa da biste dodali drugi." + "message": "Dosegnuto je ograničenje računa. Odjavi se s računa za dodavanje sljedećeg." }, "active": { - "message": "Aktivan" + "message": "aktivan" }, "locked": { - "message": "Zaključan" + "message": "zaključan" }, "unlocked": { - "message": "Otključan" + "message": "otključan" }, "server": { - "message": "Poslužitelj" + "message": "poslužitelj" }, "hostedAt": { "message": "domaćin na" }, "useDeviceOrHardwareKey": { - "message": "Koristite svoj uređaj ili hardverski ključ" + "message": "Koristi svoj uređaj ili hardverski ključ" }, "justOnce": { "message": "Samo jednom" @@ -3277,94 +3647,112 @@ "message": "Uvijek za ovu stranicu" }, "domainAddedToExcludedDomains": { - "message": "$DOMAIN$ dodano u izuzete domene.", + "message": "$DOMAIN$ dodana u izuzete domene.", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, "commonImportFormats": { - "message": "Common formats", + "message": "Uobičajeni oblici", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Nastavi na postavke preglednika?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Nastavi u centar za pomoć?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" + }, + "confirmContinueToHelpCenterPasswordManagementContent": { + "message": "Promijeni postavke auto-ispune i upravljanja lozinkama preglednika.", + "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": "Možeš vidjeti i postaviti prečace proširenja u postavkama preglednika.", + "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": "Promijeni postavke auto-ispune i upravljanja lozinkama u preglednika.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" + }, + "confirmContinueToBrowserKeyboardShortcutSettingsContent": { + "message": "Možeš vidjeti i postaviti prečace proširenja u postavkama preglednika.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { - "message": "Make Bitwarden your default password manager?", + "message": "Postavi Bitwarden kao zadani upravitelj lozinki?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ostavljanje ove postavke isključenom može uzrokovati sukob između prijedloga za auto-ispunu Bitwardena i tvojeg preglednika.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { - "message": "Make Bitwarden your default password manager", + "message": "Postavi Bitwarden kao zadani upravitelj lozinki", "description": "Label for the setting that allows overriding the default browser autofill settings" }, "privacyPermissionAdditionNotGrantedTitle": { - "message": "Unable to set Bitwarden as the default password manager", + "message": "Nije moguće postaviti Bitwarden kao zadani upravitelj lozinki", "description": "Title for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "privacyPermissionAdditionNotGrantedDescription": { - "message": "You must grant browser privacy permissions to Bitwarden to set it as the default password manager.", + "message": "Za postavljanje Bitwardena kao zadanog upravitelja lozinki moraš pregledniku dati dopuštenje privatnosti.", "description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "makeDefault": { - "message": "Make default", + "message": "Postavi kao zadano", "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { - "message": "Credentials saved successfully!", + "message": "Vjerodajnice uspješno spremljene!", + "description": "Notification message for when saving credentials has succeeded." + }, + "passwordSaved": { + "message": "Lozinka pohranjena!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { - "message": "Credentials updated successfully!", + "message": "Vjerodajnice uspješno ažurirane!", + "description": "Notification message for when updating credentials has succeeded." + }, + "passwordUpdated": { + "message": "Lozinka ažurirana!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { - "message": "Error saving credentials. Check console for details.", + "message": "Greška pri spremanju vjerodajnica. Za detalje pogledaj konzolu.", "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Uspješno" }, "removePasskey": { - "message": "Remove passkey" + "message": "Ukloni pristupni ključ" }, "passkeyRemoved": { - "message": "Passkey removed" - }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + "message": "Pristupni ključ uklonjen" }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Prijedlozi auto-ispune" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Spremi u auto-ispunu stavku prijave za ovu stranicu" }, "yourVaultIsEmpty": { - "message": "Your vault is empty" + "message": "Tvoj trezor je prazan" }, "noItemsMatchSearch": { - "message": "No items match your search" + "message": "Nema stavki podudarnih s pretragom" }, "clearFiltersOrTryAnother": { - "message": "Clear filters or try another search term" + "message": "Očisti filtre ili pokušaj s drugačijom pretragom" }, "copyInfoTitle": { - "message": "Copy info - $ITEMNAME$", + "message": "Kopiraj informacije - $ITEMNAME$", "description": "Title for a button that opens a menu with options to copy information from an item.", "placeholders": { "itemname": { @@ -3374,7 +3762,7 @@ } }, "copyNoteTitle": { - "message": "Copy Note - $ITEMNAME$", + "message": "Kopiraj bilješku - $ITEMNAME$", "description": "Title for a button copies a note to the clipboard.", "placeholders": { "itemname": { @@ -3384,7 +3772,7 @@ } }, "moreOptionsLabel": { - "message": "More options, $ITEMNAME$", + "message": "Više mogućnosti, $ITEMNAME$", "description": "Aria label for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -3394,7 +3782,7 @@ } }, "moreOptionsTitle": { - "message": "More options - $ITEMNAME$", + "message": "Više mogućnosti - $ITEMNAME$", "description": "Title for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -3404,7 +3792,7 @@ } }, "viewItemTitle": { - "message": "View item - $ITEMNAME$", + "message": "Pogledaj stavku - $ITEMNAME$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Auto-ispuna - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3424,40 +3812,40 @@ } }, "noValuesToCopy": { - "message": "No values to copy" + "message": "Nema vrijednosti za kopiranje" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Dodijeli zbirkama" }, "copyEmail": { - "message": "Copy email" + "message": "Kopiraj e-poštu" }, "copyPhone": { - "message": "Copy phone" + "message": "Kopiraj telefon" }, "copyAddress": { - "message": "Copy address" + "message": "Kopiraj adresu" }, "adminConsole": { - "message": "Admin Console" + "message": "Konzola administratora" }, "accountSecurity": { - "message": "Account security" + "message": "Sigurnost računa" }, "notifications": { - "message": "Notifications" + "message": "Obavijesti" }, "appearance": { - "message": "Appearance" + "message": "Izgled" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Greška pri dodjeljivanju ciljne zbirke." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Greška pri dodjeljivanju ciljne mape." }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "Pogledaj stavku u $NAME$", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -3467,7 +3855,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "Natrag na $NAME$", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -3477,10 +3865,10 @@ } }, "new": { - "message": "New" + "message": "Novo" }, "removeItem": { - "message": "Remove $NAME$", + "message": "Ukloni $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -3490,16 +3878,16 @@ } }, "itemsWithNoFolder": { - "message": "Items with no folder" + "message": "Stavke bez mape" }, "itemDetails": { - "message": "Item details" + "message": "Detalji stavke" }, "itemName": { - "message": "Item name" + "message": "Naziv stavke" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "S dopuštenjima samo za prikaz ne možeš ukloniti zbirke: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3508,29 +3896,47 @@ } }, "organizationIsDeactivated": { - "message": "Organization is deactivated" + "message": "Organizacija je deaktivirana" }, "owner": { - "message": "Owner" + "message": "Vlasnik" }, "selfOwnershipLabel": { - "message": "You", + "message": "Ti", "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": "Ne može se pristupiti stavkama u deaktiviranoj Organizaciji. Kontaktiraj vlasnika Organizacije za pomoć." + }, + "additionalInformation": { + "message": "Dodatne informacije" + }, + "itemHistory": { + "message": "Povijest stavke" + }, + "lastEdited": { + "message": "Zadnje uređeno" + }, + "ownerYou": { + "message": "Vlasnik: Ti" + }, + "linked": { + "message": "Povezano" + }, + "copySuccessful": { + "message": "Kopiranje uspješno" }, "upload": { - "message": "Upload" + "message": "Prijenos" }, "addAttachment": { - "message": "Add attachment" + "message": "Dodaj privitak" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "Najveća veličina datoteke je 500 MB" }, "deleteAttachmentName": { - "message": "Delete attachment $NAME$", + "message": "Izbriši privitak", "placeholders": { "name": { "content": "$1", @@ -3539,7 +3945,7 @@ } }, "downloadAttachmentName": { - "message": "Download $NAME$", + "message": "Preuzmi $NAME$", "placeholders": { "name": { "content": "$1", @@ -3548,15 +3954,389 @@ } }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Are you sure you want to permanently delete this attachment?" + "message": "Sigurno želiš trajno izbrisati ovaj privitak?" }, "premium": { "message": "Premium" }, "freeOrgsCannotUseAttachments": { - "message": "Free organizations cannot use attachments" + "message": "Besplatne organizacije ne mogu koristiti privitke" }, "filters": { - "message": "Filters" + "message": "Filtri" + }, + "personalDetails": { + "message": "Osobni podaci" + }, + "identification": { + "message": "Identifikacija" + }, + "contactInfo": { + "message": "Kontakt podaci" + }, + "downloadAttachment": { + "message": "Preuzmi - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "broj kartice završava na", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Vjerodajnice za prijavu" + }, + "authenticatorKey": { + "message": "Kôd za provjeru" + }, + "autofillOptions": { + "message": "Postavke auto-ispune" + }, + "websiteUri": { + "message": "Web stranica (URI)" + }, + "websiteUriCount": { + "message": "Broj URI-ja: $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": "Web stranica dodana" + }, + "addWebsite": { + "message": "Dodaj web stranicu" + }, + "deleteWebsite": { + "message": "Izbriši web stranicu" + }, + "defaultLabel": { + "message": "Zadano ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Prikaži otkrivanje podudaranja $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Sakrij otkrivanje podudaranja $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Auto-ispuna kod učitavanja?" + }, + "cardExpiredTitle": { + "message": "Istekla kartica" + }, + "cardExpiredMessage": { + "message": "Ako je obnovljeno, ažuriraj podatke o kartici" + }, + "cardDetails": { + "message": "Detalji kartice" + }, + "cardBrandDetails": { + "message": "$BRAND$ detalji", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Omogući animacije" + }, + "addAccount": { + "message": "Dodaj račun" + }, + "loading": { + "message": "Učitavanje" + }, + "data": { + "message": "Podaci" + }, + "passkeys": { + "message": "Pristupni ključevi", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Lozinke", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Prijava pristupnim ključem", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, + "assign": { + "message": "Dodijeli" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Samo članovi organizacije s pristupom ovim zbirkama će moći vidjeti stavku." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Samo članovi organizacije s pristupom ovim zbirkama će moći vidjeti stavke." + }, + "bulkCollectionAssignmentWarning": { + "message": "Odabrano je $TOTAL_COUNT$ stavki. Nije moguće ažurirati $READONLY_COUNT$ stavki jer nemaš dopuštenje za uređivanje.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Dodaj polje" + }, + "add": { + "message": "Dodaj" + }, + "fieldType": { + "message": "Vrsta polja" + }, + "fieldLabel": { + "message": "Oznaka polja" + }, + "textHelpText": { + "message": "Koristi tekstualna polja za podatke poput sigurnosnih pitanja" + }, + "hiddenHelpText": { + "message": "Koristi skrivena polja za osjetljive podatke poput lozinke" + }, + "checkBoxHelpText": { + "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." + }, + "linkedLabelHelpText": { + "message": "Unesi html id polja, naziv, aria-label ili rezervirano mjesto." + }, + "editField": { + "message": "Uredi polje" + }, + "editFieldLabel": { + "message": "Uredi $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Obriši $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ dodana", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Ponovno poredaj $LABEL$. Koristi tipke sa strelicom za pomicanje stavke gore ili dolje.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ pomaknut gore, pozicija $INDEX$ od $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Odaberi zbirke za dodjelu" + }, + "personalItemTransferWarningSingular": { + "message": "1 stavka će biti trajno prenesena u odabranu organizaciju. Više nećeš posjedovati ovu stavku." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ stavke/i će biti trajno prenesene u odabranu organizaciju. Više nećeš posjedovati ove stavke.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 stavka će biti trajno prenesena u $ORG$. Više nećeš posjedovati ovu stavku.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ stavke/i će biti trajno prenesene u $ORG$. Više nećeš posjedovati ove stavke.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Zbirke uspješno dodijeljene" + }, + "nothingSelected": { + "message": "Ništa nije odabrano." + }, + "movedItemsToOrg": { + "message": "Odabrane stavke premještene u $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Stavke premještene u $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Stavka premještena u $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ pomaknuto dolje, pozicija $INDEX$ od$LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Lokacija stavke" + }, + "fileSends": { + "message": "Send datoteke" + }, + "textSends": { + "message": "Send tekstovi" + }, + "bitwardenNewLook": { + "message": "Bitwarden ima novi izgled!" + }, + "bitwardenNewLookDesc": { + "message": "Auto-ispuna i pretraga iz kartice Trezor je lakša i intuitivnija nego ikad prije. Razgledaj!" + }, + "accountActions": { + "message": "Radnje na računu" + }, + "showNumberOfAutofillSuggestions": { + "message": "Prikaži broj prijedloga auto-ispune na ikoni proširenja" + }, + "systemDefault": { + "message": "Zadano sustavom" + }, + "enterprisePolicyRequirementsApplied": { + "message": "Pravila tvrtke primijenjena su na ovu postavku" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Stavke u smeću" + }, + "noItemsInTrash": { + "message": "Nema stavki u smeću" + }, + "noItemsInTrashDesc": { + "message": "Stavke koje obrišeš biti će premještene ovdje, a nakon 30 dana biti će trajno izbrisane" + }, + "trashWarning": { + "message": "Stavke koje se nalaze u Smeću duže od 30 dana će biti automatski izbrisane" + }, + "restore": { + "message": "Vrati" + }, + "deleteForever": { + "message": "Izbriši zauvijek" + }, + "noEditPermissions": { + "message": "Nemaš prava za uređivanje ove stavke" } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index cb84bac73c6..ba551e0fa3b 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Bejelentkezés vagy új fiók létrehozása a biztonsági széf eléréséhez." }, + "inviteAccepted": { + "message": "A meghívás elfogadásra került." + }, "createAccount": { "message": "Fiók létrehozása" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "A fiók létrehozásának befejezése jelszó beállításával" }, - "login": { - "message": "Bejelentkezés" - }, "enterpriseSingleSignOn": { "message": "Vállalati önálló bejelentkezés" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Mesterjelszó emlékeztető (nem kötelező)" }, + "joinOrganization": { + "message": "Csatlakozás szervezethez" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Fejezzük be a szervezethez csatlakozást egy mesterjelszó beállításával." + }, "tab": { "message": "Fül" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Biztonsági kód másolása" }, + "copyName": { + "message": "Név másolása" + }, + "copyCompany": { + "message": "Cég másolása" + }, + "copySSN": { + "message": "Társadalombiztosítási szám másolása" + }, + "copyPassportNumber": { + "message": "Útlevélszám másolása" + }, + "copyLicenseNumber": { + "message": "Licensz szám másolása" + }, "autoFill": { "message": "Automatikus kitöltés" }, @@ -150,7 +171,7 @@ "message": "Bejelentkezés a saját széfbe" }, "autoFillInfo": { - "message": "Nincsenek elérhető bejelentkezések ehhez a fülhöz ezért az automatikus kitöltés nem működik." + "message": "Nincsenek elérhető bejelentkezések az automatikus kitöltéshez az aktuális böngészőfülnél." }, "addLogin": { "message": "Bejelentkezés hozzáadása" @@ -280,6 +301,24 @@ "editFolder": { "message": "Mappa szerkesztése" }, + "newFolder": { + "message": "Új mappa" + }, + "folderName": { + "message": "Mappanév" + }, + "folderHintText": { + "message": "Mappa beágyazása a szülőmappa nevének hozzáadásával, majd egy “/” karakterrel. Példa: Közösségi/Fórumok" + }, + "noFoldersAdded": { + "message": "Nem lett mappa hozzáadva." + }, + "createFoldersToOrganize": { + "message": "Hozzunk létre mappákat a széfelemek rendszerezéséhez" + }, + "deleteFolderPermanently": { + "message": "Biztosan véglegesen törlésre kerüljön ez a mappa?" + }, "deleteFolder": { "message": "Mappa törlése" }, @@ -345,16 +384,56 @@ "message": "Minimum jelszó hosszúság" }, "uppercase": { - "message": "Nagybetűs (A-Z)" + "message": "Nagybetűs (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Kisbetűs (a-z)" + "message": "Kisbetűs (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Számok (0-9)" + "message": "Számok (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Speciális karakterek (!@#$%^&*)" + "message": "Speciális karakterek (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Bevonás", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Nagybetűs karakterek bevonása", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Kisbetűs karakterek bevonása", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Számok bevonása", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Speciális karakterek bevonása", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Szavak száma" @@ -376,7 +455,12 @@ "message": "Minimális speciális" }, "avoidAmbChar": { - "message": "Félreérthető karakterek mellőzése" + "message": "Félreérthető karakterek mellőzése", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Félreérthető karakterek mellőzése", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Keresés a széfben" @@ -556,6 +640,18 @@ "security": { "message": "Biztonság" }, + "confirmMasterPassword": { + "message": "Mesterjelszó megerősítése" + }, + "masterPassword": { + "message": "Mesterjelszó" + }, + "masterPassImportant": { + "message": "A mesterjelszó nem állítható helyre, ha elfelejtik!" + }, + "masterPassHintLabel": { + "message": "Mesterjelszó emlékeztető" + }, "errorOccurred": { "message": "Hiba történt." }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Felhasználódat létrehoztuk. Most már be tudsz jelentkezni." }, + "newAccountCreated2": { + "message": "Az új fiók létrrejött." + }, + "youHaveBeenLoggedIn": { + "message": "Megtörtént a bejelentkezés!" + }, "youSuccessfullyLoggedIn": { "message": "A bejelentkezés sikeres volt." }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Ellenőrző kód szükséges." }, + "webauthnCancelOrTimeout": { + "message": "A hitelesítés megszakításra került vagy túl sokáig tartott. Próbáljuk újra." + }, "invalidVerificationCode": { "message": "Érvénytelen ellenőrző kód" }, @@ -613,7 +718,7 @@ } }, "autofillError": { - "message": "Nem sikerült automatikusan kitölteni a bejelentkezést ezen a weboldalon. Helyette másold/illeszt be a felhasználóneved és/vagy a jelszavadat." + "message": "Nem sikerült automatikusan kitölteni a kiválasztott elemet ezen az oldalon. Helyette vágólapon keresztül kell bemásolni." }, "totpCaptureError": { "message": "Az aktuális weboldalól nem lehet szkennelni a QR kódot." @@ -624,6 +729,18 @@ "totpCapture": { "message": "Hitelesítő QR kód szkennelése az aktuális weboldalról" }, + "totpHelperTitle": { + "message": "Tegyük zökkenőmentessé a kétlépcsős azonosítást." + }, + "totpHelper": { + "message": "A Bitwarden képes tárolni és kitölteni a kétlépcsős ellenőrző kódokat. Másoljuk ki és illesszük be a kulcsot ebbe a mezőbe." + }, + "totpHelperWithCapture": { + "message": "A Bitwarden képes tárolni és kitölteni a kétlépcsős ellenőrző kódokat. Válasszuk a kamera ikont, hogy képernyőképet készítsünk a webhely hitelesítő QR kódjáról vagy másoljuk ki és illesszük be a kulcsot ebbe a mezőbe." + }, + "learnMoreAboutAuthenticators": { + "message": "További információ a hitelesítőkről" + }, "copyTOTP": { "message": "Hitelesítő kód másolása (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Bejelentkezési munkamenete lejárt." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Biztos benne, hogy ki szeretnél jelentkezni?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Új URI" }, + "addDomain": { + "message": "Tartomány hozzáadása", + "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": { "message": "Az elem hozzáadásra került." }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Bejelentkezés hozzáadás kérése" }, + "vaultSaveOptionsTitle": { + "message": "Mentés széf opciókba" + }, "addLoginNotificationDesc": { "message": "A \"Bejelentkezés értesítés hozzáadása\" automatikusan felajánlja a bejelentkezés széfbe mentését az első bejelentkezéskor." }, "addLoginNotificationDescAlt": { "message": "Egy elem hozzáadásának kérése, ha az nem található a széfben. Minden bejelentkezett fiókra vonatkozik." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Kártyák megjelenítése a Fül oldalon" }, "showCardsCurrentTabDesc": { "message": "Kártyaelemek listázása a Fül oldalon a könnyű automatikus kitöltéshez." }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" + }, "showIdentitiesCurrentTab": { "message": "Azonosítások megjelenítése a Fül oldalon" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Alapértelmezett URI egyezés érzékelés", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Az URI egyezés érzékelés alapértelmezett módjának kiválasztása a bejelentkezéseknél olyan műveletek esetében mint az automatikus kitöltés." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB titkosított tárhely a fájlmellékleteknek." }, + "premiumSignUpEmergency": { + "message": "Sürgősségi hozzáférés" + }, "premiumSignUpTwoStepOptions": { "message": "Saját kétlépcsős bejelentkezési lehetőségek mint a YubiKey és a Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "A prémium tagság megvásárolható a bitwarden.com webes széfben. Szeretnénk felkeresni a webhelyet most?" }, + "premiumPurchaseAlertV2": { + "message": "Prémium szolgáltatást vásárolhatunk a Bitwarden webalkalmazás fiókbeállításai között." + }, "premiumCurrentMember": { "message": "Prémium tag vagyunk!" }, "premiumCurrentMemberThanks": { "message": "Köszönjük a Bitwarden támogatását." }, + "premiumFeatures": { + "message": "Áttérés prémium verzióra és fogadás:" + }, "premiumPrice": { "message": "Mindez csak $PRICE$ /év.", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "Mindez csak $PRICE$ /év!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Frissítés megtörtént" }, @@ -1179,10 +1342,19 @@ }, "showAutoFillMenuOnFormFields": { "message": "Automatikus kitöltés menü megjelenítése az űrlapmezőkön", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { - "message": "Minden bejelentkezett fiókra vonatkozik." + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { + "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { "message": "Az ütközések elkerülése érdekében kapcsoljuk ki a böngésző beépített jelszókezelő beállításait." @@ -1202,15 +1374,34 @@ "message": "Ha az automatikus kitöltés menü került kiválasztásra", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { - "message": "Automatikus kitöltés oldalbetöltésnél" + "message": "Automatikus kitöltés engedélyezése oldal betöltéskor" }, "enableAutoFillOnPageLoadDesc": { "message": "Ha egy bejelentkezési űrlap észlelésre került, az adatok automatikus kitöltése az oldal betöltésekor." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Az oldalbetöltésnél automatikus kitöltést a feltört vagy nem megbízhatató weboldalak kihasználhatják." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" + }, "learnMoreAboutAutofill": { "message": "További információk az automatikus kitöltésről" }, @@ -1238,9 +1429,15 @@ "commandOpenSidebar": { "message": "Széf megnyitása oldalsávon" }, - "commandAutofillDesc": { + "commandAutofillLoginDesc": { "message": "Az aktuális webhelynél az utoljára használt bejelentkezés automatikus kitöltése." }, + "commandAutofillCardDesc": { + "message": "Az aktuális webhelynél az utoljára használt kártya." + }, + "commandAutofillIdentityDesc": { + "message": "Az aktuális webhelynél az utoljára használt személyazonosító." + }, "commandGeneratePasswordDesc": { "message": "Új véletlenszerű jelszó generálása ás másolása a vágólapra." }, @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean (Logikai)" }, + "cfTypeCheckbox": { + "message": "Jelölődoboz" + }, "cfTypeLinked": { "message": "Csatolva", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "$TYPE$ megtekintése", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Jelszó előzmények" }, @@ -1533,6 +1742,10 @@ "message": "Alap domain", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Alap domain (ajánlott)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Tartománynév", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Találat érzékelés", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Alapértelmezett találat érzékelés", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Opciók váltása" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Nincsenek listázható jelszavak." }, + "clearHistory": { + "message": "Előzmények törlése" + }, + "noPasswordsToShow": { + "message": "Nincsenek megjeleníthető jelszavak." + }, + "noRecentlyGeneratedPassword": { + "message": "Mostanában nem lett jelszó generálva." + }, "remove": { "message": "Eltávolítás" }, @@ -1677,6 +1899,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Egy vagy több szervezeti szabály érinti a generátor beállításokat." }, + "passwordGenerator": { + "message": "Jelszó generátor" + }, + "usernameGenerator": { + "message": "Felhasználónév generátor" + }, + "useThisPassword": { + "message": "Jelszó használata" + }, + "useThisUsername": { + "message": "Felhasználónév használata" + }, + "securePasswordGenerated": { + "message": "A biztonságos jelszó generálásra került! Ne felejtsük el frissíteni a jelszót a webhelyen is." + }, + "useGeneratorHelpTextPartOne": { + "message": "Generátor használata", + "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": "erős egyedi jelszó létrehozásához", + "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" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Az új mesterjelszó nem felel meg a szabály követelményeknek." }, - "receiveMarketingEmails": { - "message": "Emaileket kaphatunk a Bitwardentől bejelentésekről, tanácsokról és kutatási lehetőségekről." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Leiratkozás" @@ -1874,6 +2119,12 @@ "nativeMessagingWrongUserTitle": { "message": "A fiók nem egyezik." }, + "nativeMessagingWrongUserKeyDesc": { + "message": "A biometrikus feloldás nem sikerült. A biometrikus titkos kulcs nem tudta feloldani a széfet. Próbáljuk újra beállítani a biometrikus adatokat." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "A biometrikus kulcs nem egyezik." + }, "biometricsNotEnabledTitle": { "message": "A biometrikus adatok nincsenek beüzemelve." }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Oldjuk fel a felhasználó zárolását az asztali alkalmazásban és próbáljuk újra." }, + "biometricsNotAvailableTitle": { + "message": "A biometrikus feloldás nem érhető el." + }, + "biometricsNotAvailableDesc": { + "message": "A biometrikus feloldás jelenleg nem érhető el. Próbáljuk újra később." + }, "biometricsFailedTitle": { "message": "A biometria nem sikerült." }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "A szervezeti politika blokkolta az elemek importálását az egyedi széfbe." }, + "domainsTitle": { + "message": "Tartomány", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Kizárt domainek" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "A Bitwarden nem kéri a bejelentkezési adatok mentését ezeknél a tartományoknál az összes bejelentkezési fiókra vonatkozva. A változtatások életbe lépéséhez frissíteni kell az oldalt." }, + "websiteItemLabel": { + "message": "Webhely $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ nem érvényes domain.", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "A kizárt tartomány módosítások mentésre kerültek." + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Jelszóval védett" }, + "copyLink": { + "message": "Hivatkozás másolása" + }, "copySendLink": { "message": "Send hivatkozás másolása", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "A Send létrejött.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "A Send sikeresen létrejött!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "A Send bárki számára elérhető a hivatkozással a következő $DAYS$ napban.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "A Send hivatkozás másolásra került.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "A Send mentésre került.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Email hitelesítés szükséges" }, + "emailVerifiedV2": { + "message": "Az email cím ellenőrzésre került." + }, "emailVerificationRequiredDesc": { "message": "A funkció használatához igazolni kell email címet. Az email cím a webtárban ellenőrizhető." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatikus regisztráció" }, @@ -2629,13 +2929,22 @@ "autofillSettings": { "message": "Automatikus kitöltés beállítások" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Bullenytűparancsok kezelése" + }, "autofillShortcut": { "message": "Automatikus kitöltés billentyűparancs" }, - "autofillShortcutNotSet": { + "autofillLoginShortcutNotSet": { "message": "Az automatikus kitöltés billentyűzetparancs nincs beállítva. Módosítsuk ezt a böngésző beállításaiban." }, - "autofillShortcutText": { + "autofillLoginShortcutText": { "message": "Az automatikus kitöltés billentyűparancsa: $COMMAND$. Módosítsuk ezt a böngésző beállításaiban.", "placeholders": { "command": { @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Az eszköz megbízható." }, + "sendsNoItemsTitle": { + "message": "Nincsenek natív Send elemek.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "A Send használatával biztonságosan megoszthatjuk a titkosított információkat bárkivel.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Az adatbevitel kötelező." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 mező igényel figyelmet.." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ mező igényel figyelmet.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Választás --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "A mesterjelszót újra bekérő elemeket nem lehet automatikusan kitölteni az oldal betöltésekor. Az automatikus kitöltés az oldal betöltésekor kikapcsolásra kerül.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "Az automatikus kitöltés az oldal betöltésekor az alapértelmezett beállítás használatára lett beállítva.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Kapcsoljuk ki a mesterjelszó újbóli bekérését a mező szerkesztéséhez.", @@ -2896,11 +3225,11 @@ "message": "Ugrás a tartalomra" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { @@ -2911,10 +3240,18 @@ "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Oldjuk fel a fiók zárolását az automatikus kitöltési javaslatok megtekintéséhez.", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Fiók feloldása", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Oldjuk fel a fiók zárolását, új ablakban nyílik meg.", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Töltse kia hitelesítő adatokat", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Elem hozzáadása", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "Új bejelentkezés", + "description": "Button text to display within inline menu when there are no matching items on a login field" + }, + "addNewLoginItemAria": { + "message": "Új széf bejelentkezési elem hozzáadása, új ablakban nyílik meg.", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "Új kártya", + "description": "Button text to display within inline menu when there are no matching items on a credit card field" + }, + "addNewCardItemAria": { + "message": "Új széf kártyaelem hozzáadása, új ablakban nyílik meg.", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "Új személyazonosság", + "description": "Button text to display within inline menu when there are no matching items on an identity field" + }, + "addNewIdentityItemAria": { + "message": "Új széf személyazonosság elem hozzáadása, új ablakban nyílik meg.", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Indítsuk el a DUO-t és kövessük a lépéseket a bejelentkezés befejezéséhez." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "A fájl jelszó érvénytelen. Használjuk az exportfájl létrehozásakor megadott jelszót." }, - "importDestination": { - "message": "Importálás leírás" + "destination": { + "message": "Cél" }, "learnAboutImportOptions": { "message": "Információ az importálási opciókról" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "A kezdeményező hely által megkövetelt ellenőrzés. Ez a szolgáltatás még nincs megvalósítva mesterjelszó nélküli fiókok esetén." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Bejelentkezés hozzáférési kulccsal?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "Nincs megfelelő bejelentkezés ehhez a webhelyhez." }, + "noMatchingLoginsForSite": { + "message": "Nincsenek egyező bejelentkezések ehhez a webhelyhez." + }, "confirm": { "message": "Megerősítés" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Hozzáférési kulcs mentése új bejelentkezésként" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Bejelentkezés választás a hozzáférési kulcs mentéséhez" }, + "chooseCipherForPasskeyAuth": { + "message": "Hozzáférési kulcs választás a bejelentkezéshez" + }, "passkeyItem": { "message": "Hozzáférési kulcs elem" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,6 +3659,30 @@ "message": "Általános formátumok", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Legyen a Bitwarden az alapértelmezett jelszókezelő?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" @@ -3317,10 +3711,18 @@ "message": "A hitelesítések sikeresen mentésre kerültek.", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "A jelszó mentésre került!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "A hitelesítések sikeresen frissítésre kerültek.", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "A jelszó frissítésre került!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Hiba történt a hitelesítések mentésekor. A részletekért ellenőrizzük a konzolt.", "description": "Notification message for when saving credentials has failed." @@ -3334,20 +3736,6 @@ "passkeyRemoved": { "message": "A jelszó eltávolításra került." }, - "unassignedItemsBannerNotice": { - "message": "Megjegyzés: A nem hozzárendelt szervezeti elemek már nem láthatók az Összes széf nézetben a különböző eszközökön és csak az Adminisztrátori konzolon keresztül érhetők el." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Megjegyzés: 2024. május 16-tól a nem hozzárendelt szervezeti elemek többé nem lesznek láthatók az Összes széf nézetben a különböző eszközökön és csak az Adminisztrátori konzolon keresztül lesznek elérhetők." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Rendeljük hozzá ezeket az elemeket a gyűjteményhez", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "a láthatósághoz.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Automatikus kitöltés javaslatok" }, @@ -3415,7 +3803,7 @@ }, "autofillTitle": { "message": "Automatikus kitöltés - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "Nincsenek másolandó értékek." }, - "assignCollections": { - "message": "Gyűjtemények hozzárendelése" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Email cím másolása" @@ -3493,13 +3881,13 @@ "message": "Items with no folder" }, "itemDetails": { - "message": "Item details" + "message": "Elem részletek" }, "itemName": { - "message": "Item name" + "message": "Elem neve" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Nem távolíthatók el a csak megtekintési engedéllyel bíró gyűjtemények: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3899,33 @@ "message": "Organization is deactivated" }, "owner": { - "message": "Owner" + "message": "Tulajdonos" }, "selfOwnershipLabel": { - "message": "You", + "message": "Saját magam", "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." }, + "additionalInformation": { + "message": "További információ" + }, + "itemHistory": { + "message": "Elem előzmény" + }, + "lastEdited": { + "message": "Utoljára szerkesztve" + }, + "ownerYou": { + "message": "Tulajdonos: Én" + }, + "linked": { + "message": "Csatolva" + }, + "copySuccessful": { + "message": "A másolás sikeres volt." + }, "upload": { "message": "Feltöltés" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Szűrők" + }, + "personalDetails": { + "message": "Személyes adatok" + }, + "identification": { + "message": "Azonosítás" + }, + "contactInfo": { + "message": "Kapcsolat infó" + }, + "downloadAttachment": { + "message": "Letöltés - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "kártyaszám végződés:", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Bejelentkezési hitelesítések" + }, + "authenticatorKey": { + "message": "Hitelesítő kulcs" + }, + "autofillOptions": { + "message": "Automatikus kitöltés opciók" + }, + "websiteUri": { + "message": "Webhely (URI)" + }, + "websiteUriCount": { + "message": "Webhely (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": "A webhely hozzáadásra került." + }, + "addWebsite": { + "message": "Webhely hozzáadása" + }, + "deleteWebsite": { + "message": "Webhely törlése" + }, + "defaultLabel": { + "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": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "$WEBSITE$ egyező érzékelés megjelenítése", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "$WEBSITE$ egyező érzékelés elrejtése", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Automatikus kitöltés oldalbetöltésnél?" + }, + "cardExpiredTitle": { + "message": "Lejárt kártya" + }, + "cardExpiredMessage": { + "message": "Ha megújítottuk, frissítsük a kártya adatait." + }, + "cardDetails": { + "message": "Kártyaadatok" + }, + "cardBrandDetails": { + "message": "$BRAND$ adatok", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Animációk engedélyezése" + }, + "addAccount": { + "message": "Fiók hozzáadása" + }, + "loading": { + "message": "A betöltés folyamatban van." + }, + "data": { + "message": "Adat" + }, + "passkeys": { + "message": "Hozzáférési kulcsok", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Jelszavak", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Bejelentkezés hozzáférési kulccsal", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, + "assign": { + "message": "Assign" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Csak az ezekhez a gyűjteményekhez hozzáféréssel rendelkező szervezeti tagok láthatják az elemet." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Csak az ezekhez a gyűjteményekhez hozzáféréssel rendelkező szervezeti tagok láthatják az elemeket." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Mező hozzáadása" + }, + "add": { + "message": "Hozzáadás" + }, + "fieldType": { + "message": "Mezőtípus" + }, + "fieldLabel": { + "message": "Mezőfelirat" + }, + "textHelpText": { + "message": "Szövegmezők használata olyan adatokhoz mint a biztonsági kérdések" + }, + "hiddenHelpText": { + "message": "Rejtett mezők használata olyan érzékeny adatokhoz mint a jelszó" + }, + "checkBoxHelpText": { + "message": "Jelölődobozok használata, ha automatikusan ki szeretnénk tölteni olyan űrlap jelölődobozt mint az email cím megjegyzése" + }, + "linkedHelpText": { + "message": "Csatolt mező használata, ha egy adott webhely automatikus kitöltésével kapcsolatos problémákat tapasztalunk." + }, + "linkedLabelHelpText": { + "message": "Adjuk meg a mező html azonosítóját, nevét, aria címkéjét vagy helyőrét." + }, + "editField": { + "message": "Mező szerkesztése" + }, + "editFieldLabel": { + "message": "$LABEL$ szerkesztése", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "$LABEL$ törlése", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ hozzáadásra került.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "$LABEL$ átrendezése. A nyílbillentyűkkel mozgassuk az elemet felfelé vagy lefelé.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ feljebb került, $INDEX$/$LENGTH$ pozícióba", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ lejjebb került, $INDEX$/$LENGTH$ pozícióba", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Elem helyek" + }, + "fileSends": { + "message": "Fájl küldés" + }, + "textSends": { + "message": "Szöveg küldés" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" + }, + "accountActions": { + "message": "Fiókműveletek" + }, + "showNumberOfAutofillSuggestions": { + "message": "Az automatikus bejelentkezési kitöltési javaslatok számának megjelenítése a bővítmény ikonján" + }, + "systemDefault": { + "message": "Rendszer alapértelmezett" + }, + "enterprisePolicyRequirementsApplied": { + "message": "Erre a beállításra a vállalkozás rendszabály követelmények lettek alkalmazva." + }, + "fileSavedToDevice": { + "message": "A fájl mentésre került az eszközre. Kezeljük az eszközről a letöltéseket." + }, + "showCharacterCount": { + "message": "Karakterszámláló megjelenítése" + }, + "hideCharacterCount": { + "message": "Karakterszámláló elrejtése" + }, + "itemsInTrash": { + "message": "elem van a lomtárban." + }, + "noItemsInTrash": { + "message": "Nincs elem a lomtárban." + }, + "noItemsInTrashDesc": { + "message": "A törölt elemek itt jelennek meg és 30 nap elteltével véglegesen törlődnek." + }, + "trashWarning": { + "message": "A 30 napnál régebben lomtárba került elemek automatikusan törlésre kerülnek." + }, + "restore": { + "message": "Visszaállítás" + }, + "deleteForever": { + "message": "Végleges törlés" + }, + "noEditPermissions": { + "message": "Nincs jogosulltság ezen elem szerkesztéséheu." } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index aafc33e818f..b43eddb3fdc 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Masuk atau buat akun baru untuk mengakses brankas Anda." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "Buat Akun" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "Masuk" - }, "enterpriseSingleSignOn": { "message": "SSO Perusahaan" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Petunjuk Kata Sandi Utama (opsional)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "Tab" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Salin Kode Keamanan" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "Isi otomatis" }, @@ -150,7 +171,7 @@ "message": "Masuk ke brankas Anda" }, "autoFillInfo": { - "message": "Tidak ada info masuk yang tersedia untuk mengisi secara otomatis tab peramban saat ini." + "message": "Tidak ada info masuk yang tersedia untuk mengisi sexara otomatis tab peramban saat ini." }, "addLogin": { "message": "Tambah Info Masuk" @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "Panjang kata sandi minimum" }, "uppercase": { - "message": "Huruf besar (A-Z)" + "message": "Huruf besar (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Huruf kecil (a-z)" + "message": "Huruf kecil (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Angka (0-9)" + "message": "Angka (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "karakter khusus (contoh.! @#$%^&*)" + "message": "karakter khusus (contoh.! @#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Jumlah Kata" @@ -376,7 +455,12 @@ "message": "Spesial Minimum" }, "avoidAmbChar": { - "message": "Hindari Karakter Ambigu" + "message": "Hindari Karakter Ambigu", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Cari brankas" @@ -556,6 +640,18 @@ "security": { "message": "Keamanan" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Terjadi kesalahan" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Akun baru Anda telah dibuat! Sekarang Anda bisa masuk." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "Anda berhasil masuk" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Kode verifikasi diperlukan." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Kode verifikasi tidak valid" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Pindai kode QR autentikator dari laman ini" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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": "Salin kunci Autentikator (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Sesi masuk Anda telah berakhir." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Anda yakin ingin keluar?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "URl Baru" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "Item yang Ditambahkan" }, @@ -737,23 +873,32 @@ "enableAddLoginNotification": { "message": "Tanya untuk penambahan login" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "\"Notifikasi Penambahan Info Masuk\" secara otomatis akan meminta Anda untuk menyimpan info masuk baru ke brankas Anda saat Anda masuk untuk pertama kalinya." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Show cards on Tab page" }, "showCardsCurrentTabDesc": { - "message": "List card items on the Tab page for easy auto-fill." + "message": "List card items on the Tab page for easy autofill." + }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" }, "showIdentitiesCurrentTabDesc": { - "message": "List identity items on the Tab page for easy auto-fill." + "message": "List identity items on the Tab page for easy autofill." }, "clearClipboard": { "message": "Hapus Papan Klip", @@ -791,7 +936,7 @@ "message": "Iya, Perbarui Sekarang" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Unlock" @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Deteksi Kecocokan URI Bawaan", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Pilih cara bawaan penanganan pencocokan URI untuk masuk saat melakukan tindakan seperti isi-otomatis." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB penyimpanan berkas yang dienkripsi." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Anda dapat membeli keanggotaan premium di brankas web bitwarden.com. Anda ingin mengunjungi situs web sekarang?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Anda adalah anggota premium!" }, "premiumCurrentMemberThanks": { "message": "Terima kasih telah mendukung Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "Semua itu hanya $PRICE$ /tahun!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Penyegaran selesai" }, @@ -1178,14 +1341,23 @@ "message": "URL dari semua lingkungan telah disimpan." }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,20 +1371,39 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "Aktifkan Isi-Otomatis Saat Memuat Laman" }, "enableAutoFillOnPageLoadDesc": { "message": "Jika formulir info masuk terdeteksi, secara otomatis melakukan pengisian otomatis ketika memuat laman web." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Compromised or untrusted websites can exploit autofill on page load." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" }, "learnMoreAboutAutofill": { - "message": "Learn more about auto-fill" + "message": "Learn more about autofill" }, "defaultAutoFillOnPageLoad": { "message": "Konfigurasi autofill standard untuk item login." @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Buka brankas di bilah samping" }, - "commandAutofillDesc": { - "message": "Isi otomatis info masuk yang digunakan terakhir untuk situs ini" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Buat dan salin kata sandi acak baru ke papan klip." @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Terhubung", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Riwayat Kata Sandi" }, @@ -1533,6 +1742,10 @@ "message": "Domain basis", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Nama Domain", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Deteksi Kecocokan", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Deteksi kecocokan standar", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Ubah Opsi" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Tidak ada sandi yang dapat dicantumkan." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Hapus" }, @@ -1677,6 +1899,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" }, @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Kata sandi utama Anda yang baru tidak memenuhi persyaratan kebijakan." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Domain yang Dikecualikan" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ bukan domain yang valid", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Dilindungi kata sandi" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Salin tautan Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send Dibuat ", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send diedit", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Verifikasi Email Diperlukan" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Anda harus memverifikasi email Anda untuk menggunakan fitur ini. Anda dapat memverifikasi email Anda di brankas web." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Pendaftaran Otomatis" }, @@ -2606,10 +2906,10 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { - "message": "How to auto-fill" + "message": "How to autofill" }, "autofillSelectInfoWithCommand": { "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", @@ -2627,16 +2927,25 @@ "message": "Got it" }, "autofillSettings": { - "message": "Auto-fill settings" + "message": "Autofill settings" + }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 6bf7864adac..79c4d3cff74 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -13,17 +13,17 @@ "loginOrCreateNewAccount": { "message": "Accedi o crea un nuovo account per accedere alla tua cassaforte." }, + "inviteAccepted": { + "message": "Invito accettato" + }, "createAccount": { "message": "Crea account" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Imposta una password robusta" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" - }, - "login": { - "message": "Accedi" + "message": "Termina la creazione del tuo account impostando una password" }, "enterpriseSingleSignOn": { "message": "Single Sign-On aziendale" @@ -50,7 +50,7 @@ "message": "Un suggerimento per la password principale può aiutarti a ricordarla se la dimentichi." }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Se dimentichi la password, il suggerimento password può essere inviato alla tua email. $CURRENT$/$MAXIMUM$ massimo carattere.", "placeholders": { "current": { "content": "$1", @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Suggerimento per la password principale (facoltativo)" }, + "joinOrganization": { + "message": "Unisciti all'organizzazione" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Termina l'adesione a questa organizzazione impostando una password principale." + }, "tab": { "message": "Scheda" }, @@ -107,17 +113,32 @@ "copySecurityCode": { "message": "Copia codice di sicurezza" }, + "copyName": { + "message": "Copia nome" + }, + "copyCompany": { + "message": "Copia azienda" + }, + "copySSN": { + "message": "Copia Codice fiscale/Previdenza sociale" + }, + "copyPassportNumber": { + "message": "Copia numero passaporto" + }, + "copyLicenseNumber": { + "message": "Copia numero licenza" + }, "autoFill": { "message": "Riempimento automatico" }, "autoFillLogin": { - "message": "Riempi automaticamente login" + "message": "Autocompletamento login" }, "autoFillCard": { - "message": "Riempi automaticamente carta" + "message": "Autocompletamento carta" }, "autoFillIdentity": { - "message": "Riempi automaticamente identità" + "message": "Autocompletamento identità" }, "generatePasswordCopied": { "message": "Genera password e copiala" @@ -280,6 +301,24 @@ "editFolder": { "message": "Modifica cartella" }, + "newFolder": { + "message": "Nuova cartella" + }, + "folderName": { + "message": "Nome cartella" + }, + "folderHintText": { + "message": "Annida una cartella aggiungendo il nome della cartella superiore seguito da un “/”. Esempio: Social/Forums" + }, + "noFoldersAdded": { + "message": "Nessuna cartella aggiunta" + }, + "createFoldersToOrganize": { + "message": "Crea cartelle per organizzare gli elementi della cassaforte" + }, + "deleteFolderPermanently": { + "message": "Sei sicuro di voler eliminare definitivamente questo cartella?" + }, "deleteFolder": { "message": "Elimina cartella" }, @@ -345,16 +384,56 @@ "message": "Lunghezza minima della password" }, "uppercase": { - "message": "Maiuscole (A-Z)" + "message": "Maiuscole (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Minuscole (a-z)" + "message": "Minuscole (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numeri (0-9)" + "message": "Numeri (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Caratteri speciali (!@#$%^&*)" + "message": "Caratteri speciali (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Includi", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Includi caratteri maiuscoli", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Includi caratteri minuscoli", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Includi numeri", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Includi caratteri speciali", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Numero di parole" @@ -376,7 +455,12 @@ "message": "Minimo caratteri speciali" }, "avoidAmbChar": { - "message": "Evita caratteri ambigui" + "message": "Evita caratteri ambigui", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Evita caratteri ambigui", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Cerca nella cassaforte" @@ -412,10 +496,10 @@ "message": "Rimuovi dai preferiti" }, "itemAddedToFavorites": { - "message": "Item added to favorites" + "message": "Elementi aggiunti ai preferiti" }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "Elementi rimossi dai preferiti" }, "notes": { "message": "Note" @@ -556,6 +640,18 @@ "security": { "message": "Sicurezza" }, + "confirmMasterPassword": { + "message": "Conferma password principale" + }, + "masterPassword": { + "message": "Password principale" + }, + "masterPassImportant": { + "message": "La tua password principale non può essere recuperata se la dimentichi!" + }, + "masterPassHintLabel": { + "message": "Suggerimento per la password principale" + }, "errorOccurred": { "message": "Si è verificato un errore" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Il tuo nuovo account è stato creato! Ora puoi eseguire l'accesso." }, + "newAccountCreated2": { + "message": "Il tuo nuovo account è stato creato!" + }, + "youHaveBeenLoggedIn": { + "message": "Hai effettuato l'accesso!" + }, "youSuccessfullyLoggedIn": { "message": "Hai effettuato l'accesso" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Il codice di verifica è obbligatorio." }, + "webauthnCancelOrTimeout": { + "message": "L'autenticazione è stata annullata o ha richiesto troppo tempo. Per favore riprova." + }, "invalidVerificationCode": { "message": "Codice di verifica non valido" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scansiona il codice QR dell'autenticatore da questa pagina web" }, + "totpHelperTitle": { + "message": "Rendi la 2FA facile" + }, + "totpHelper": { + "message": "Bitwarden può memorizzare e autocompletare codici di verifica 2FA. Copia e incolla la chiave in questo campo." + }, + "totpHelperWithCapture": { + "message": "Bitwarden può memorizzare e autocompletare codici di verifica 2FA. Selezionare l'icona della fotocamera per creare uno screenshot del codice QR dell'autenticatore di questo sito web, oppure copia e incolla la chiave in questo campo." + }, + "learnMoreAboutAuthenticators": { + "message": "Ulteriori informazioni sugli autenticatori" + }, "copyTOTP": { "message": "Copia la chiave di autenticazione (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "La tua sessione è scaduta." }, + "logIn": { + "message": "Accedi" + }, + "restartRegistration": { + "message": "Riprova la registrazione" + }, + "expiredLink": { + "message": "Link scaduto" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Riavvia la registrazione o prova ad accedere." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Potresti già avere un account" + }, "logOutConfirmation": { "message": "Sei sicuro di volerti disconnettere?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Nuovo URI" }, + "addDomain": { + "message": "Aggiungi dominio", + "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": { "message": "Elemento aggiunto" }, @@ -737,17 +873,26 @@ "enableAddLoginNotification": { "message": "Chiedi di aggiungere nuovi login" }, + "vaultSaveOptionsTitle": { + "message": "Salva nelle opzioni della cassaforte" + }, "addLoginNotificationDesc": { "message": "Chiedi di aggiungere un nuovo elemento se non ce n'è uno nella tua cassaforte." }, "addLoginNotificationDescAlt": { "message": "Chiedi di creare un nuovo elemento se non ce n'è uno nella tua cassaforte. Si applica a tutti gli account sul dispositivo." }, + "showCardsInVaultView": { + "message": "Mostra le carte come suggerimenti di riempimento automatico nella vista cassaforte" + }, "showCardsCurrentTab": { "message": "Mostra le carte nella sezione Scheda" }, "showCardsCurrentTabDesc": { - "message": "Mostra le carte nella sezione Scheda per riempirle automaticamente." + "message": "Mostra le carte nella sezione Scheda per un riempimento automatico più facile." + }, + "showIdentitiesInVaultView": { + "message": "Mostra le identità come suggerimenti di riempimento automatico nella vista cassaforte" }, "showIdentitiesCurrentTab": { "message": "Mostra le identità nella sezione Scheda" @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Rilevamento corrispondenza URI predefinito", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Scegli il modo predefinito in cui il rilevamento della corrispondenza URI è gestito per i login quando si eseguono azioni come il riempimento automatico." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB di spazio di archiviazione crittografato per gli allegati." }, + "premiumSignUpEmergency": { + "message": "Accesso di emergenza." + }, "premiumSignUpTwoStepOptions": { "message": "Opzioni di verifica in due passaggi proprietarie come YubiKey e Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Puoi acquistare il un abbonamento Premium dalla cassaforte web su bitwarden.com. Vuoi visitare il sito?" }, + "premiumPurchaseAlertV2": { + "message": "Puoi acquistare Premium dalle impostazioni del tuo account sull'app web Bitwarden." + }, "premiumCurrentMember": { "message": "Sei un membro Premium!" }, "premiumCurrentMemberThanks": { "message": "Grazie per il tuo supporto a Bitwarden." }, + "premiumFeatures": { + "message": "Passa a Premium e ricevi:" + }, "premiumPrice": { "message": "Il tutto per soli $PRICE$ all'anno!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "Il tutto per solo $PRICE$ all'anno!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Aggiornamento completato" }, @@ -1106,17 +1269,17 @@ "message": "App di autenticazione" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Inserisci un codice generato da un'app di autenticazione come Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP Security Key" + "message": "Chiave di sicurezza YubiKey OTP" }, "yubiKeyDesc": { "message": "Usa YubiKey per accedere al tuo account. Funziona con YubiKey 4, 4 Nano, 4C, e dispositivi NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Inserisci un codice generato da Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1133,7 +1296,7 @@ "message": "Email" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Inserisci il codice inviato alla tua email." }, "selfHostedEnvironment": { "message": "Ambiente self-hosted" @@ -1179,9 +1342,18 @@ }, "showAutoFillMenuOnFormFields": { "message": "Mostra il menu di riempimento automatico nei campi di modulo", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Suggerimenti per il riempimento automatico" + }, + "showInlineMenuLabel": { + "message": "Mostra suggerimenti di riempimento automatico nei campi del modulo" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Mostra suggerimenti quando l'icona è selezionata" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Si applica a tutti gli account sul dispositivo." }, "turnOffBrowserBuiltInPasswordManagerSettings": { @@ -1202,15 +1374,34 @@ "message": "Quando l'icona di riempimento automatico è selezionata", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, - "enableAutoFillOnPageLoad": { + "enableAutoFillOnPageLoadSectionTitle": { "message": "Riempi automaticamente al caricamento della pagina" }, + "enableAutoFillOnPageLoad": { + "message": "Abilita l'auto-completamento al caricamento della pagina" + }, "enableAutoFillOnPageLoadDesc": { "message": "Se sono rilevati campi di login, riempili automaticamente quando la pagina si carica." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Attenzione:$CLOSETAG$ Siti Web compromessi o non attendibili possono sfruttare l'auto-riempimento al caricamento della pagina.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Siti compromessi potrebbero sfruttare il riempimento automatico al caricamento della pagina." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Scopri di più sui rischi" + }, "learnMoreAboutAutofill": { "message": "Ulteriori informazioni" }, @@ -1238,9 +1429,15 @@ "commandOpenSidebar": { "message": "Apri cassaforte nella barra laterale" }, - "commandAutofillDesc": { + "commandAutofillLoginDesc": { "message": "Riempi automaticamente con l'ultimo login utilizzato sul sito corrente" }, + "commandAutofillCardDesc": { + "message": "Riempi automaticamente con l'ultima carta utilizzata sul sito corrente" + }, + "commandAutofillIdentityDesc": { + "message": "Riempi automaticamente con l'ultima identità utilizzata sul sito corrente" + }, "commandGeneratePasswordDesc": { "message": "Genera e copia una nuova password casuale negli appunti" }, @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Booleano" }, + "cfTypeCheckbox": { + "message": "Casella di controllo" + }, "cfTypeLinked": { "message": "Collegato", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1454,7 +1654,7 @@ "message": "Identità" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "Nuovo $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1463,7 +1663,16 @@ } }, "editItemHeader": { - "message": "Edit $TYPE$", + "message": "Modifica $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, + "viewItemHeader": { + "message": "Visualizza $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1481,7 +1690,7 @@ "message": "Raccolte" }, "nCollections": { - "message": "$COUNT$ collections", + "message": "$COUNT$ raccolte", "placeholders": { "count": { "content": "$1", @@ -1533,6 +1742,10 @@ "message": "Dominio di base", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Dominio di base (raccomandato)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Nome dominio", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Rilevamento di corrispondenza", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Rilevamento di corrispondenza predefinito", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Mostra/nascondi" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Non ci sono password da mostrare." }, + "clearHistory": { + "message": "Cancella cronologia" + }, + "noPasswordsToShow": { + "message": "Nessuna password da mostrare" + }, + "noRecentlyGeneratedPassword": { + "message": "Non hai generato una password di recente" + }, "remove": { "message": "Rimuovi" }, @@ -1677,6 +1899,29 @@ "passwordGeneratorPolicyInEffect": { "message": "Una o più politiche dell'organizzazione stanno influenzando le impostazioni del tuo generatore." }, + "passwordGenerator": { + "message": "Generatore di password" + }, + "usernameGenerator": { + "message": "Generatore di nomi utente" + }, + "useThisPassword": { + "message": "Usa questa password" + }, + "useThisUsername": { + "message": "Usa questo nome utente" + }, + "securePasswordGenerated": { + "message": "Password sicura generata! Non dimenticare di aggiornare la tua password anche sul sito web." + }, + "useGeneratorHelpTextPartOne": { + "message": "Usa il generatore", + "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": "per creare una password univoca robusta", + "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" }, @@ -1725,7 +1970,7 @@ "message": "Elemento riempito automaticamente e URI salvato" }, "autoFillSuccess": { - "message": "Elemento riempito automaticamente " + "message": "Elemento riempito automaticamente" }, "insecurePageWarning": { "message": "Attenzione: questa è una pagina HTTP non protetta, e tutte le informazioni che invii potrebbero essere viste e modificate da altri. Questo login è stato originariamente salvato su una pagina sicura (HTTPS)." @@ -1799,20 +2044,20 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "La tua nuova password principale non soddisfa i requisiti di sicurezza." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Ottieni consigli, annunci e opportunità di ricerca da Bitwarden nella tua casella di posta." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "Annulla iscrizione" }, "atAnyTime": { - "message": "at any time." + "message": "in qualsiasi momento." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Continuando accetti le" }, "and": { - "message": "and" + "message": "e" }, "acceptPolicies": { "message": "Selezionando questa casella accetti quanto segue:" @@ -1874,6 +2119,12 @@ "nativeMessagingWrongUserTitle": { "message": "Account non corrispondono" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Sblocco biometrico non riuscito. La chiave segreta biometrica non è riuscita a sbloccare la cassaforte. Riprova a configurare nuovamente la biometria." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Chiave biometrica non corrispondente" + }, "biometricsNotEnabledTitle": { "message": "Autenticazione biometrica non abilitata" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Sblocca questo utente nell'applicazione desktop e riprova." }, + "biometricsNotAvailableTitle": { + "message": "Sblocco biometrico non disponibile" + }, + "biometricsNotAvailableDesc": { + "message": "Lo sblocco biometrico non è attualmente disponibile. Riprova più tardi." + }, "biometricsFailedTitle": { "message": "Autenticazione biometrica fallita" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "Una politica dell'organizzazione ti impedisce di importare elementi nella tua cassaforte individuale." }, + "domainsTitle": { + "message": "Domini", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Domini esclusi" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden non chiederà di salvare le credenziali di accesso per questi domini per tutti gli account sul dispositivo. Ricarica la pagina affinché le modifiche abbiano effetto." }, + "websiteItemLabel": { + "message": "Sito $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ non è un dominio valido", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Modifiche del dominio escluso salvate" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Protetto da password" }, + "copyLink": { + "message": "Copia link" + }, "copySendLink": { "message": "Copia link del Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send creato", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send creato con successo!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "Il Send sarà disponibile a qualsiasi utente con il link per i prossimi $DAYS$ giorni.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Link del Send copiato", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send salvato", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Verifica email obbligatoria" }, + "emailVerifiedV2": { + "message": "Email verificata" + }, "emailVerificationRequiredDesc": { "message": "Devi verificare la tua email per usare questa funzionalità. Puoi verificare la tua email nella cassaforte web." }, @@ -2179,6 +2476,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": "La tua organizzazione ha disabilitato la crittografia affidabile del dispositivo. Per favore imposta una password principale per accedere alla tua cassaforte." + }, "resetPasswordPolicyAutoEnroll": { "message": "Iscrizione automatica" }, @@ -2606,7 +2906,7 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Le politiche della tua organizzazione hanno abilitato il riempimento automatico al caricamento della pagina." + "message": "Le politiche della tua organizzazione hanno abilitato l'autocompletamento al caricamento della pagina." }, "howToAutofill": { "message": "Come riempire automaticamente" @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Impostazioni di riempimento automatico" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Scorciatoia auto-riempimento" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Cambia scorciatoia" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Gestisci scorciatoia" + }, "autofillShortcut": { "message": "Scorciatoia da tastiera per riempire automaticamente" }, - "autofillShortcutNotSet": { - "message": "Non è stata impostata nessuna scorciatoia da tastiera per riempire automaticamente. Impostala nelle impostazioni del browser." + "autofillLoginShortcutNotSet": { + "message": "Non è stata impostata nessuna scorciatoia per il riempimento automatico. Cambiala nelle impostazioni del browser." }, - "autofillShortcutText": { - "message": "La scorciatoia da tastiera per riempire automaticamente è: $COMMAND$. Cambiala nelle impostazioni del browser.", + "autofillLoginShortcutText": { + "message": "La scorciatoia per l'auto-riempimento è $COMMAND$.\nGestisci tutte le scorciatoie dalle impostazioni del browser.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Dispositivo fidato" }, + "sendsNoItemsTitle": { + "message": "Nessun Send attivo", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Utilizza un Send per condividere in modo sicuro le informazioni con qualsiasi utente.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input obbligatorio." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 campo richiede tua attenzione." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ campi richiedono la tua attenzione.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Seleziona --" }, @@ -2879,18 +3208,18 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Gli elementi che richiedono di inserire di nuovo la password principale non possono essere riempiti automaticamente al caricamento della pagina.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "Riempimento automatico al caricamento della pagina impostato con l'impostazione predefinita.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Disattiva l'inserimento della password principale di nuovo per modificare questo campo", "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "Attiva/Disattiva navigazione laterale" }, "skipToContent": { "message": "Vai al contenuto" @@ -2911,10 +3240,18 @@ "message": "Sblocca il tuo account per visualizzare i login corrispondenti", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Sblocca il tuo account per visualizzare i suggerimenti di riempimento automatico", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Sblocca account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Sblocca il tuo account, apri in una nuova finestra", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Riempi le credenziali per", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Aggiungi un nuovo elemento alla cassaforte", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "Nuovo login", + "description": "Button text to display within inline menu when there are no matching items on a login field" + }, + "addNewLoginItemAria": { + "message": "Aggiungi un nuovo elemento \"login\" alla cassaforte, apri in una nuova finestra", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "Nuova carta", + "description": "Button text to display within inline menu when there are no matching items on a credit card field" + }, + "addNewCardItemAria": { + "message": "Aggiungi un nuovo elemento \"carta\" alla cassaforte, apri in una nuova finestra", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "Nuova identità", + "description": "Button text to display within inline menu when there are no matching items on an identity field" + }, + "addNewIdentityItemAria": { + "message": "Aggiungi un nuovo elemento \"identità\" alla cassaforte, apri in una nuova finestra", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Il menu di riempimento automatico di Bitwarden è disponibile. Premi il tasto freccia giù per selezionare.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Errore di connessione con il servizio Duo. Utilizza un metodo di login in due passaggi diverso o contatta Duo per assistenza." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Avvia DUO e segui i passaggi per finire di accedere." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Password errata, usa la password che hai inserito alla creazione del file di esportazione." }, - "importDestination": { - "message": "Destinazione dell'importazione" + "destination": { + "message": "Destinazione" }, "learnAboutImportOptions": { "message": "Ulteriori informazioni sulle tue opzioni di importazione" @@ -3108,7 +3472,7 @@ "message": "Conferma password del file" }, "exportSuccess": { - "message": "Vault data exported" + "message": "Dati della cassaforte esportati" }, "typePasskey": { "message": "Passkey" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verifica richiesta dal sito web. Questa funzionalità non è ancora implementata per gli account senza password principale." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Accedi con passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "Non hai un elemento corrispondente per questo sito." }, + "noMatchingLoginsForSite": { + "message": "Nessun login corrispondente per questo sito" + }, "confirm": { "message": "Conferma" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Salva la passkey come nuovo elemento" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Scegli un elemento in cui salvare questa passkey" }, + "chooseCipherForPasskeyAuth": { + "message": "Scegli una password con cui accedere" + }, "passkeyItem": { "message": "Passkey" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,6 +3659,30 @@ "message": "Formati comuni", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continua sulle impostazioni del browser?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continua sul centro assistenza?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" + }, + "confirmContinueToHelpCenterPasswordManagementContent": { + "message": "Modifica le impostazioni di riempimento automatico e di gestione delle password del browser.", + "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": "È possibile visualizzare e impostare le scorciatoie per l'estensione nelle impostazioni del browser.", + "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": "Modifica le impostazioni di riempimento automatico e di gestione delle password del browser.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" + }, + "confirmContinueToBrowserKeyboardShortcutSettingsContent": { + "message": "È possibile visualizzare e impostare le scorciatoie per l'estensione nelle impostazioni del browser.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Rendere Bitwarden il tuo password manager predefinito?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" @@ -3317,10 +3711,18 @@ "message": "Credenziali salvate!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password salvata!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credenziali aggiornate!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password aggiornata!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Errore durante il salvataggio delle credenziali. Controlla la console per più dettagli.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey rimossa" }, - "unassignedItemsBannerNotice": { - "message": "Avviso: gli elementi dell'organizzazione non assegnati non sono più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e sono ora accessibili solo tramite la Console di amministrazione." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Avviso: dal 16 maggio 2024, gli elementi dell'organizzazione non assegnati non saranno più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e saranno accessibili solo tramite la Console di amministrazione." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assegna questi elementi ad una raccolta dalla", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "per renderli visibili.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Suggerimenti per il riempimento automatico" }, "autofillSuggestionsTip": { - "message": "Salva un elemento di accesso per questo sito da riempire automaticamente" + "message": "Salva un elemento login per questo sito da riempire automaticamente" }, "yourVaultIsEmpty": { "message": "La tua cassaforte è vuota" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Riempi automaticamente - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3424,10 +3812,10 @@ } }, "noValuesToCopy": { - "message": "No values to copy" + "message": "Nessun valore da copiare" }, - "assignCollections": { - "message": "Assegna raccolte" + "assignToCollections": { + "message": "Assegna alle raccolte" }, "copyEmail": { "message": "Copia email" @@ -3493,13 +3881,13 @@ "message": "Elementi senza cartella" }, "itemDetails": { - "message": "Item details" + "message": "Dettagli elemento" }, "itemName": { - "message": "Item name" + "message": "Nome elemento" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Non puoi rimuovere raccolte con i soli permessi di visualizzazione: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,26 +3899,44 @@ "message": "L'organizzazione è disattivata" }, "owner": { - "message": "Owner" + "message": "Proprietario" }, "selfOwnershipLabel": { - "message": "You", + "message": "Tu", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Non puoi accedere agli elementi nelle organizzazioni disattivate. Contatta il proprietario della tua organizzazione per ricevere assistenza." }, + "additionalInformation": { + "message": "Informazioni aggiuntive" + }, + "itemHistory": { + "message": "Cronologia elemento" + }, + "lastEdited": { + "message": "Ultima modifica" + }, + "ownerYou": { + "message": "Proprietario: Tu" + }, + "linked": { + "message": "Collegato" + }, + "copySuccessful": { + "message": "Copia Riuscita" + }, "upload": { - "message": "Upload" + "message": "Carica" }, "addAttachment": { - "message": "Add attachment" + "message": "Aggiungi allegato" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "La dimensione massima del file è 500 MB" }, "deleteAttachmentName": { - "message": "Delete attachment $NAME$", + "message": "Elimina allegato $NAME$", "placeholders": { "name": { "content": "$1", @@ -3539,7 +3945,7 @@ } }, "downloadAttachmentName": { - "message": "Download $NAME$", + "message": "Scarica $NAME$", "placeholders": { "name": { "content": "$1", @@ -3548,15 +3954,389 @@ } }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Are you sure you want to permanently delete this attachment?" + "message": "Sei sicuro di voler eliminare definitivamente questo allegato?" }, "premium": { "message": "Premium" }, "freeOrgsCannotUseAttachments": { - "message": "Free organizations cannot use attachments" + "message": "Le organizzazioni gratis non possono utilizzare gli allegati" }, "filters": { - "message": "Filters" + "message": "Filtri" + }, + "personalDetails": { + "message": "Dati personali" + }, + "identification": { + "message": "Identificativo" + }, + "contactInfo": { + "message": "Info di contatto" + }, + "downloadAttachment": { + "message": "Scarica - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "il numero di carta termina con", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Credenziali di accesso" + }, + "authenticatorKey": { + "message": "Chiave di autenticazione" + }, + "autofillOptions": { + "message": "Opzioni di riempimento automatico" + }, + "websiteUri": { + "message": "Sito Web (URI)" + }, + "websiteUriCount": { + "message": "Sito Web (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": "Sito web aggiunto" + }, + "addWebsite": { + "message": "Aggiungi sito web" + }, + "deleteWebsite": { + "message": "Elimina sito web" + }, + "defaultLabel": { + "message": "Predefinito ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Mostra corrispondenza $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Nascondi corrispondenza $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Riempi automaticamente al caricamento della pagina?" + }, + "cardExpiredTitle": { + "message": "Carta scaduta" + }, + "cardExpiredMessage": { + "message": "Se hai rinnovato la carta, aggiorna le informazioni" + }, + "cardDetails": { + "message": "Dati della carta" + }, + "cardBrandDetails": { + "message": "Dati del $BRAND$", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Abilita animazioni" + }, + "addAccount": { + "message": "Aggiungi account" + }, + "loading": { + "message": "Caricamento in corso..." + }, + "data": { + "message": "Dati" + }, + "passkeys": { + "message": "Passkey", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Password", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Accedi con passkey", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, + "assign": { + "message": "Assegna" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Solo i membri dell'organizzazione con accesso a queste raccolte saranno in grado di vedere l'elemento." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Solo i membri dell'organizzazione con accesso a queste raccolte saranno in grado di vedere gli elementi." + }, + "bulkCollectionAssignmentWarning": { + "message": "Hai selezionato $TOTAL_COUNT$ elementi. Non puoi aggiornare $READONLY_COUNT$ elementi perché non hai l'autorizzazione per modificarli.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Aggiungi campo" + }, + "add": { + "message": "Aggiungi" + }, + "fieldType": { + "message": "Tipo campo" + }, + "fieldLabel": { + "message": "Etichetta campo" + }, + "textHelpText": { + "message": "Usa campi di testo per dati come domande di sicurezza" + }, + "hiddenHelpText": { + "message": "Usa i campi nascosti per dati sensibili come una password" + }, + "checkBoxHelpText": { + "message": "Usa le caselle di controllo se vuoi riempire automaticamente la casella di controllo di un modulo, come una email da ricordare" + }, + "linkedHelpText": { + "message": "Utilizzare un campo collegato quando si verificano problemi di riempimento automatico per un sito web specifico." + }, + "linkedLabelHelpText": { + "message": "Inserisci l'id html del campo, il nome, l'aria-label o il segnaposto." + }, + "editField": { + "message": "Modifica campo" + }, + "editFieldLabel": { + "message": "Modifica $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Elimina $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ aggiunto", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Riordina $LABEL$. Utilizza i tasti freccia per spostare l'elemento sopra o sotto.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ spostato su, in posizione $INDEX$ di $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Seleziona le raccolte da assegnare" + }, + "personalItemTransferWarningSingular": { + "message": "1 elemento verrà trasferito definitivamente all'organizzazione selezionata. Non possiederai più questo elemento." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ verranno trasferiti definitivamente all'organizzazione selezionata. Non possiederai più questi elementi.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 elemento verrà trasferito definitivamente a $ORG$. Non possiederai più questo elemento.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ verranno trasferiti definitivamente a $ORG$. Non possiederai più questi elementi.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Raccolte assegnate con successo" + }, + "nothingSelected": { + "message": "Non hai selezionato nulla." + }, + "movedItemsToOrg": { + "message": "Elementi selezionati spostati in $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Elementi spostati su $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Elemento spostato su $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ spostato giù, in posizione $INDEX$ di $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Posizione elemento" + }, + "fileSends": { + "message": "Send File" + }, + "textSends": { + "message": "Send Testo" + }, + "bitwardenNewLook": { + "message": "Bitwarden ha un nuovo look!" + }, + "bitwardenNewLookDesc": { + "message": "È più facile e intuitivo che mai utilizzare il riempimento automatico e cercare dalla scheda Cassaforte. Dai un'occhiata!" + }, + "accountActions": { + "message": "Azioni dell'account" + }, + "showNumberOfAutofillSuggestions": { + "message": "Mostra il numero di suggerimenti di riempimento automatico sull'icona dell'estensione" + }, + "systemDefault": { + "message": "Predefinito del sistema" + }, + "enterprisePolicyRequirementsApplied": { + "message": "I requisiti della policy aziendale sono stati applicati a questa impostazione" + }, + "fileSavedToDevice": { + "message": "File salvato sul dispositivo. Gestisci dai download del dispositivo." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Elementi nel cestino" + }, + "noItemsInTrash": { + "message": "Nessun elemento nel cestino" + }, + "noItemsInTrashDesc": { + "message": "Gli elementi cancellati appariranno qui e saranno eliminati definitivamente dopo 30 giorni" + }, + "trashWarning": { + "message": "Gli elementi nel cestino saranno eliminati automaticamente dopo 30 giorni" + }, + "restore": { + "message": "Ripristina" + }, + "deleteForever": { + "message": "Elimina definitivamente" + }, + "noEditPermissions": { + "message": "Non hai i permessi per modificare questo elemento" } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index aba509d766c..393a8311779 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "安全なデータ保管庫へアクセスするためにログインまたはアカウントを作成してください。" }, + "inviteAccepted": { + "message": "招待が承認されました" + }, "createAccount": { "message": "アカウントの作成" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "パスワードを設定してアカウントの作成を完了してください" }, - "login": { - "message": "ログイン" - }, "enterpriseSingleSignOn": { "message": "組織のシングルサインオン" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "マスターパスワードのヒント (省略可能)" }, + "joinOrganization": { + "message": "組織に参加" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "マスターパスワードを設定して、この組織への参加を完了します。" + }, "tab": { "message": "タブ" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "セキュリティコードをコピー" }, + "copyName": { + "message": "名前をコピー" + }, + "copyCompany": { + "message": "会社名をコピー" + }, + "copySSN": { + "message": "社会保障番号をコピー" + }, + "copyPassportNumber": { + "message": "パスポート番号をコピー" + }, + "copyLicenseNumber": { + "message": "免許証番号をコピー" + }, "autoFill": { "message": "自動入力" }, @@ -150,7 +171,7 @@ "message": "保管庫にログイン" }, "autoFillInfo": { - "message": "現在のブラウザタブに自動入力するログイン情報はありません。" + "message": "現在のブラウザタブに自動入力するログインはありません。" }, "addLogin": { "message": "ログイン情報を追加" @@ -280,6 +301,24 @@ "editFolder": { "message": "フォルダーを編集" }, + "newFolder": { + "message": "新しいフォルダー" + }, + "folderName": { + "message": "フォルダー名" + }, + "folderHintText": { + "message": "親フォルダーの名前の後に「/」を追加するとフォルダをネストします。例: ソーシャル/フォーラム" + }, + "noFoldersAdded": { + "message": "フォルダーが追加されていません" + }, + "createFoldersToOrganize": { + "message": "保管庫のアイテムを整理するフォルダーを作成します" + }, + "deleteFolderPermanently": { + "message": "このフォルダーを完全に削除しますか?" + }, "deleteFolder": { "message": "フォルダーを削除" }, @@ -345,16 +384,56 @@ "message": "パスワードの最低文字数" }, "uppercase": { - "message": "大文字(A-Z)" + "message": "大文字(A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "小文字(a-z)" + "message": "小文字(a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "数字 (0~9)" + "message": "数字 (0~9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "特殊文字(!@#$%^&*)" + "message": "特殊文字(!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "含む文字", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "大文字を含める", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A~Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "小文字を含める", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "数字を含める", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0~9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "特殊記号を含める", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "単語数" @@ -376,7 +455,12 @@ "message": "記号の最小数" }, "avoidAmbChar": { - "message": "あいまいな文字を省く" + "message": "あいまいな文字を省く", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "あいまいな文字を避ける", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "保管庫を検索" @@ -556,6 +640,18 @@ "security": { "message": "セキュリティ" }, + "confirmMasterPassword": { + "message": "マスターパスワードの確認" + }, + "masterPassword": { + "message": "マスターパスワード" + }, + "masterPassImportant": { + "message": "マスターパスワードを忘れた場合は復元できません!" + }, + "masterPassHintLabel": { + "message": "マスターパスワードのヒント" + }, "errorOccurred": { "message": "エラーが発生しました" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "新しいアカウントを作成しました!今すぐログインできます。" }, + "newAccountCreated2": { + "message": "新しいアカウントを作成しました!" + }, + "youHaveBeenLoggedIn": { + "message": "ログインしました!" + }, "youSuccessfullyLoggedIn": { "message": "ログインに成功しました" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "認証コードは必須項目です。" }, + "webauthnCancelOrTimeout": { + "message": "認証がキャンセルされたか、時間がかかりすぎました。もう一度やり直してください。" + }, "invalidVerificationCode": { "message": "認証コードが間違っています" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "現在のウェブページから認証 QR コードをスキャンする" }, + "totpHelperTitle": { + "message": "2段階認証をシームレスにする" + }, + "totpHelper": { + "message": "Bitwarden は2段階認証コードを保存・入力できます。この欄にキーをコピーして貼り付けてください。" + }, + "totpHelperWithCapture": { + "message": "Bitwarden は2段階認証コードを保存・入力できます。 カメラアイコンを選択して、このウェブサイトの認証 QR コードのスクリーンショットを撮るか、キーをコピーしてこのフィールドに貼り付けてください。" + }, + "learnMoreAboutAuthenticators": { + "message": "認証方法の詳細" + }, "copyTOTP": { "message": "認証キーのコピー (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "ログインセッションの有効期限が切れています。" }, + "logIn": { + "message": "ログイン" + }, + "restartRegistration": { + "message": "登録を再度始める" + }, + "expiredLink": { + "message": "期限切れのリンク" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "登録を再度始めるか、ログインしてください。" + }, + "youMayAlreadyHaveAnAccount": { + "message": "すでにアカウントを持っている可能性があります" + }, "logOutConfirmation": { "message": "ログアウトしてもよろしいですか?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "新しい URI" }, + "addDomain": { + "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": { "message": "追加されたアイテム" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "ログイン情報の追加を尋ねる" }, + "vaultSaveOptionsTitle": { + "message": "保管庫のオプションに保存" + }, "addLoginNotificationDesc": { "message": "初めてログインしたとき保管庫にログイン情報を保存するよう「ログイン情報を追加」通知を自動的に表示します。" }, "addLoginNotificationDescAlt": { "message": "保管庫にアイテムが見つからない場合は、アイテムを追加するよう要求します。ログインしているすべてのアカウントに適用されます。" }, + "showCardsInVaultView": { + "message": "保管庫ビューに自動入力の候補としてカードを表示する" + }, "showCardsCurrentTab": { "message": "タブページにカードを表示" }, "showCardsCurrentTabDesc": { "message": "自動入力を簡単にするために、タブページにカードアイテムを表示します" }, + "showIdentitiesInVaultView": { + "message": "保管庫ビューに自動入力の候補として ID を表示する" + }, "showIdentitiesCurrentTab": { "message": "タブページに ID を表示" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "デフォルトの URI 一致検出方法", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "自動入力などのアクションをする時に、デフォルトでどの方法で URI の一致を検出するか選択します。" @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1GB の暗号化されたファイルストレージ" }, + "premiumSignUpEmergency": { + "message": "緊急アクセス" + }, "premiumSignUpTwoStepOptions": { "message": "YubiKey、Duo などのプロプライエタリな2段階認証オプション。" }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "プレミアム会員権は bitwarden.com ウェブ保管庫で購入できます。ウェブサイトを開きますか?" }, + "premiumPurchaseAlertV2": { + "message": "Bitwarden ウェブアプリでアカウント設定からプレミアムを購入できます。" + }, "premiumCurrentMember": { "message": "あなたはプレミアム会員です!" }, "premiumCurrentMemberThanks": { "message": "Bitwarden を支援いただき、ありがとうございます。" }, + "premiumFeatures": { + "message": "プレミアムにアップグレードして受け取る:" + }, "premiumPrice": { "message": "全部でなんと$PRICE$/年だけ!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "すべて込みで年間たったの$PRICE$!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "更新完了" }, @@ -1179,9 +1342,18 @@ }, "showAutoFillMenuOnFormFields": { "message": "フォーム項目に自動入力メニューを表示", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "自動入力の候補" + }, + "showInlineMenuLabel": { + "message": "フォームフィールドに自動入力の候補を表示する" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "アイコンが選択されているときに候補を表示する" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "ログインしているすべてのアカウントに適用されます。" }, "turnOffBrowserBuiltInPasswordManagerSettings": { @@ -1202,15 +1374,34 @@ "message": "自動入力アイコンを選択しているとき", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "ページ読み込み時に自動入力する" + }, "enableAutoFillOnPageLoad": { "message": "ページ読み込み時の自動入力を有効化" }, "enableAutoFillOnPageLoadDesc": { "message": "ページ読み込み時にログインフォームを検出したとき、ログイン情報を自動入力します。" }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$警告:$CLOSETAG$ 侵害された、または信頼できないウェブサイトは、ページ読み込み時の自動入力を悪用する可能性があります。", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "ウイルス感染したり信頼できないウェブサイトは、ページの読み込み時の自動入力を悪用できてしまいます。" }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "リスクについての詳細" + }, "learnMoreAboutAutofill": { "message": "自動入力についての詳細" }, @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "サイドバーで保管庫を開く" }, - "commandAutofillDesc": { - "message": "現在のウェブサイトで前回使用されたログイン情報を自動入力します。" + "commandAutofillLoginDesc": { + "message": "現在のウェブサイトで前回使用されたログイン情報を自動入力します" + }, + "commandAutofillCardDesc": { + "message": "現在のウェブサイトで最後に使用されたカード情報を自動入力する" + }, + "commandAutofillIdentityDesc": { + "message": "現在のウェブサイトで最後に使用された ID を自動入力する" }, "commandGeneratePasswordDesc": { "message": "ランダムなパスワードを生成してクリップボードにコピーします。" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "真偽値" }, + "cfTypeCheckbox": { + "message": "チェックボックス" + }, "cfTypeLinked": { "message": "リンク済", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "$TYPE$ を表示", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "パスワードの履歴" }, @@ -1533,6 +1742,10 @@ "message": "ベースドメイン", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "ベースドメイン (推奨)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "ドメイン名", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "一致検出方法", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "デフォルトの一致検出方法", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "オプションの切り替え" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "表示するパスワードがありません" }, + "clearHistory": { + "message": "履歴を消去" + }, + "noPasswordsToShow": { + "message": "パスワードがありません" + }, + "noRecentlyGeneratedPassword": { + "message": "最近パスワードを生成していません" + }, "remove": { "message": "削除" }, @@ -1677,6 +1899,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": "保管庫タイムアウト時のアクション" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "新しいマスターパスワードは最低要件を満たしていません。" }, - "receiveMarketingEmails": { - "message": "Bitwarden からのお知らせ、アドバイス、アンケート調査等のメールを受信します。" + "receiveMarketingEmailsV2": { + "message": "Bitwarden からメールでアドバイスやお知らせ、リサーチの機会を受け取りましょう。" }, "unsubscribe": { "message": "配信停止" @@ -1874,6 +2119,12 @@ "nativeMessagingWrongUserTitle": { "message": "アカウントが一致しません" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "生体認証のロック解除に失敗しました。生体認証キーでの保管庫のロック解除に失敗しました。生体認証を再度設定してください。" + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "生体認証キーが一致しません" + }, "biometricsNotEnabledTitle": { "message": "生体認証が有効になっていません" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "デスクトップアプリでこのユーザーのロックを解除して、もう一度やり直してください。" }, + "biometricsNotAvailableTitle": { + "message": "生体認証ロック解除が利用できません" + }, + "biometricsNotAvailableDesc": { + "message": "生体認証ロック解除は現在利用できません。しばらくしてからもう一度お試しください。" + }, "biometricsFailedTitle": { "message": "生体認証に失敗しました" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "組織のポリシーにより、個々の保管庫へのアイテムのインポートがブロックされました。" }, + "domainsTitle": { + "message": "ドメイン", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "除外するドメイン" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden はログインしているすべてのアカウントで、これらのドメインのログイン情報を保存するよう要求しません。 変更を有効にするにはページを更新する必要があります。" }, + "websiteItemLabel": { + "message": "ウェブサイト $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ は有効なドメインではありません", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "除外ドメインの変更を保存しました" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "パスワード保護あり" }, + "copyLink": { + "message": "リンクをコピー" + }, "copySendLink": { "message": "Send リンクをコピー", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "作成した Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send を作成しました!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "Send は、次の $DAYS$ 日間はリンクを知っている人全員が利用できます。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send リンクをコピーしました", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "編集済みの Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "メールアドレスの確認が必要です" }, + "emailVerifiedV2": { + "message": "メールアドレスを認証しました" + }, "emailVerificationRequiredDesc": { "message": "この機能を使用するにはメールアドレスを確認する必要があります。ウェブ保管庫でメールアドレスを確認できます。" }, @@ -2179,6 +2476,9 @@ "updateWeakMasterPasswordWarning": { "message": "あなたのマスターパスワードは、組織のポリシーを満たしていません。保管庫にアクセスするには、今すぐマスターパスワードを更新する必要があります。この操作を続けると、現在のセッションがログアウトされ、再ログインする必要があります。他のデバイスでのアクティブなセッションは最大1時間継続する場合があります。" }, + "tdeDisabledMasterPasswordRequired": { + "message": "あなたの組織は信頼できるデバイスの暗号化を無効化しました。保管庫にアクセスするにはマスターパスワードを設定してください。" + }, "resetPasswordPolicyAutoEnroll": { "message": "自動登録" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "自動入力の設定" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "自動入力のショートカット" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "ショートカットの変更" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "ショートカットを管理" + }, "autofillShortcut": { "message": "自動入力キーボードショートカット" }, - "autofillShortcutNotSet": { + "autofillLoginShortcutNotSet": { "message": "自動入力のショートカットが設定されていません。ブラウザの設定で変更してください。" }, - "autofillShortcutText": { - "message": "自動入力のショートカットは $COMMAND$ です。ブラウザの設定でこれを変更してください。", + "autofillLoginShortcutText": { + "message": "自動入力のショートカットは $COMMAND$ です。ブラウザの設定ですべてのショートカットを管理できます。", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "信頼されたデバイス" }, + "sendsNoItemsTitle": { + "message": "アクティブな Send なし", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Send を使用すると暗号化された情報を誰とでも安全に共有できます。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "入力が必要です。" }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1フィールドは注意が必要です。" + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ フィールドは注意が必要です。", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- 選択 --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "マスターパスワードの再入力を促すアイテムは、ページ読み込み時に自動入力できません。ページ読み込み時の自動入力をオフにしました。", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "ページ読み込み時の自動入力はデフォルトの設定を使うよう設定しました。", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "このフィールドを編集するには、マスターパスワードの再入力をオフにしてください", @@ -2911,10 +3240,18 @@ "message": "一致するログイン情報を表示するには、アカウントのロックを解除してください", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "自動入力候補を表示するにはアカウントのロックを解除してください", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "アカウントのロックを解除", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "アカウントのロックを解除し、新しいウィンドウで開く", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "資格情報を入力:", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "新しい保管庫アイテムを追加", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "新規ログイン", + "description": "Button text to display within inline menu when there are no matching items on a login field" + }, + "addNewLoginItemAria": { + "message": "新しい保管庫のログインアイテムを追加し、新しいウィンドウで開く", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "新しいカード", + "description": "Button text to display within inline menu when there are no matching items on a credit card field" + }, + "addNewCardItemAria": { + "message": "新しい保管庫のカードアイテムを追加し、新しいウィンドウで開く", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "新しい ID", + "description": "Button text to display within inline menu when there are no matching items on an identity field" + }, + "addNewIdentityItemAria": { + "message": "新しい保管庫 ID アイテムを追加し、新しいウィンドウで開く", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Bitwarden 自動入力メニューがあります。下矢印キーを押すと選択できます。", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Duo サービスへの接続中にエラーが発生しました。異なる二段階ログイン方法を使用するか、Duo に連絡してください。" + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "ログインを完了するには DUO を起動し手順に従ってください。" }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "無効なファイルパスワードです。エクスポートファイルを作成したときに入力したパスワードを使用してください。" }, - "importDestination": { - "message": "インポート先" + "destination": { + "message": "保存先" }, "learnAboutImportOptions": { "message": "インポートオプションの詳細" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "開始サイトでの認証が必要です。この機能はマスターパスワードのないアカウントではまだ対応していません。" }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "パスキーでログインしますか?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "このサイトに一致するログイン情報がありません。" }, + "noMatchingLoginsForSite": { + "message": "このサイトに一致するログイン情報がありません" + }, "confirm": { "message": "確認" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "パスキーを新しいログイン情報として保存" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "このパスキーを保存するログイン情報を選択してください" }, + "chooseCipherForPasskeyAuth": { + "message": "ログインに使うパスキーを選択してください" + }, "passkeyItem": { "message": "パスキーアイテム" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,6 +3659,30 @@ "message": "一般的な形式", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "ブラウザの設定に進みますか?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "ヘルプセンターに進みますか?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" + }, + "confirmContinueToHelpCenterPasswordManagementContent": { + "message": "ブラウザの自動入力とパスワード管理設定を変更します。", + "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": "拡張機能のショートカットはブラウザの設定で表示および設定できます。", + "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": "ブラウザの自動入力とパスワード管理設定を変更します。", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" + }, + "confirmContinueToBrowserKeyboardShortcutSettingsContent": { + "message": "拡張機能のショートカットはブラウザの設定で表示および設定できます。", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Bitwarden をデフォルトのパスワードマネージャーにしますか?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" @@ -3317,10 +3711,18 @@ "message": "認証情報を保存しました!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "パスワードを保存しました!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "認証情報を更新しました!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "パスワードを更新しました!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "資格情報の保存中にエラーが発生しました。詳細はコンソールを確認してください。", "description": "Notification message for when saving credentials has failed." @@ -3334,20 +3736,6 @@ "passkeyRemoved": { "message": "パスキーを削除しました" }, - "unassignedItemsBannerNotice": { - "message": "注意: 割り当てられていない組織アイテムは、すべての保管庫ビューでは表示されなくなり、管理コンソールからのみアクセスできるようになります。" - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "お知らせ:2024年5月16日に、 割り当てられていない組織アイテムは、すべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。" - }, - "unassignedItemsBannerCTAPartOne": { - "message": "これらのアイテムのコレクションへの割り当てを", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "で実行すると表示できるようになります。", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "候補を自動入力する" }, @@ -3415,7 +3803,7 @@ }, "autofillTitle": { "message": "自動入力 - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "コピーする値がありません" }, - "assignCollections": { - "message": "コレクションを割り当て" + "assignToCollections": { + "message": "コレクションに割り当て" }, "copyEmail": { "message": "メールアドレスをコピー" @@ -3493,13 +3881,13 @@ "message": "フォルダーがないアイテム" }, "itemDetails": { - "message": "Item details" + "message": "アイテムの詳細" }, "itemName": { - "message": "Item name" + "message": "アイテム名" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "表示のみの権限が与えられているコレクションを削除することはできません: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3899,33 @@ "message": "組織は無効化されています" }, "owner": { - "message": "Owner" + "message": "所有者" }, "selfOwnershipLabel": { - "message": "You", + "message": "あなた", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "無効化された組織のアイテムにアクセスすることはできません。組織の所有者に連絡してください。" }, + "additionalInformation": { + "message": "その他の情報" + }, + "itemHistory": { + "message": "アイテム履歴" + }, + "lastEdited": { + "message": "最終更新日" + }, + "ownerYou": { + "message": "所有者: あなた" + }, + "linked": { + "message": "リンク済" + }, + "copySuccessful": { + "message": "コピーしました" + }, "upload": { "message": "アップロード" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "フィルター" + }, + "personalDetails": { + "message": "個人情報" + }, + "identification": { + "message": "ID" + }, + "contactInfo": { + "message": "連絡先情報" + }, + "downloadAttachment": { + "message": "ダウンロード - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "カード番号の末尾", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "ログイン情報" + }, + "authenticatorKey": { + "message": "認証キー" + }, + "autofillOptions": { + "message": "自動入力オプション" + }, + "websiteUri": { + "message": "ウェブサイト (URI)" + }, + "websiteUriCount": { + "message": "ウェブサイト (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": "ウェブサイトを追加しました" + }, + "addWebsite": { + "message": "ウェブサイトを追加" + }, + "deleteWebsite": { + "message": "ウェブサイトを削除" + }, + "defaultLabel": { + "message": "デフォルト ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "一致検出 $WEBSITE$を表示", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "一致検出 $WEBSITE$を非表示", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "ページ読み込み時に自動入力する" + }, + "cardExpiredTitle": { + "message": "期限切れのカード" + }, + "cardExpiredMessage": { + "message": "カードの更新があった場合、カード情報を更新してください" + }, + "cardDetails": { + "message": "カード情報" + }, + "cardBrandDetails": { + "message": "$BRAND$ の詳細", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "アニメーションを有効化" + }, + "addAccount": { + "message": "アカウントを追加" + }, + "loading": { + "message": "読み込み中" + }, + "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": "割り当て" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "これらのコレクションにアクセスできる組織メンバーのみがアイテムを見ることができます。" + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "これらのコレクションにアクセスできる組織メンバーのみがアイテムを見ることができます。" + }, + "bulkCollectionAssignmentWarning": { + "message": "$TOTAL_COUNT$ アイテムを選択しました。編集権限がないため、$READONLY_COUNT$ アイテムを更新できません。", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "フィールドを追加" + }, + "add": { + "message": "追加" + }, + "fieldType": { + "message": "フィールドタイプ" + }, + "fieldLabel": { + "message": "フィールドラベル" + }, + "textHelpText": { + "message": "セキュリティに関する質問などのデータにはテキストフィールドを使用します" + }, + "hiddenHelpText": { + "message": "パスワードのような機密データには非表示フィールドを使用します" + }, + "checkBoxHelpText": { + "message": "メールアドレスの記憶などのフォームのチェックボックスを自動入力する場合はチェックボックスを使用します" + }, + "linkedHelpText": { + "message": "特定のウェブサイトで自動入力の問題が発生している場合は、リンクされたフィールドを使用します" + }, + "linkedLabelHelpText": { + "message": "フィールドの HTML ID、名前、aria-label、またはプレースホルダを入力します" + }, + "editField": { + "message": "フィールドを編集" + }, + "editFieldLabel": { + "message": "$LABEL$ を編集", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "$LABEL$ を削除", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ を追加しました", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "$LABEL$ の順序を変更します。矢印キーを押すとアイテムを上下に移動します。", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ を上に移動しました。$INDEX$ / $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "割り当てるコレクションを選択" + }, + "personalItemTransferWarningSingular": { + "message": "1個のアイテムは選択した組織に恒久的に移行されます。このアイテムはあなたの所有ではなくなります。" + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ 個のアイテムは選択した組織に恒久的に移行されます。これらのアイテムはあなたの所有ではなくなります。", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1個のアイテムは $ORG$ に恒久的に移行されます。このアイテムはあなたの所有ではなくなります。", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ 個のアイテムは $ORG$ に恒久的に移行されます。これらのアイテムはあなたの所有ではなくなります。", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "コレクションの割り当てに成功しました" + }, + "nothingSelected": { + "message": "何も選択されていません。" + }, + "movedItemsToOrg": { + "message": "選択したアイテムを $ORGNAME$ に移動しました", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "アイテムを $ORGNAME$ に移動しました", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "アイテムを $ORGNAME$ に移動しました", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ を下に移動しました。$INDEX$ / $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "アイテムの場所" + }, + "fileSends": { + "message": "ファイル Send" + }, + "textSends": { + "message": "テキスト Send" + }, + "bitwardenNewLook": { + "message": "Bitwarden が新しい外観になりました。" + }, + "bitwardenNewLookDesc": { + "message": "保管庫タブからの自動入力と検索がこれまで以上に簡単で直感的になりました。" + }, + "accountActions": { + "message": "アカウントの操作" + }, + "showNumberOfAutofillSuggestions": { + "message": "拡張機能アイコンにログイン自動入力の候補の数を表示する" + }, + "systemDefault": { + "message": "システムのデフォルト" + }, + "enterprisePolicyRequirementsApplied": { + "message": "エンタープライズポリシー要件がこの設定に適用されました" + }, + "fileSavedToDevice": { + "message": "ファイルをデバイスに保存しました。デバイスのダウンロードで管理できます。" + }, + "showCharacterCount": { + "message": "文字数を表示" + }, + "hideCharacterCount": { + "message": "文字数を隠す" + }, + "itemsInTrash": { + "message": "ゴミ箱にあるアイテム" + }, + "noItemsInTrash": { + "message": "ゴミ箱にアイテムはありません" + }, + "noItemsInTrashDesc": { + "message": "削除したアイテムはここに表示され、30日後に完全に削除されます" + }, + "trashWarning": { + "message": "30日以上ゴミ箱にあったアイテムは自動的に削除されます" + }, + "restore": { + "message": "復元" + }, + "deleteForever": { + "message": "完全に削除" + }, + "noEditPermissions": { + "message": "このアイテムを編集する権限がありません" } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 38fe634abea..5e3809e00df 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "ანგარიშის შექმნა" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "ავტორიზაცია" - }, "enterpriseSingleSignOn": { "message": "Enterprise single sign-on" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Master password hint (optional)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "Tab" }, @@ -107,17 +113,32 @@ "copySecurityCode": { "message": "უსაფრთხოების კოდის კოპირება" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "თვითშევსება" }, "autoFillLogin": { - "message": "Auto-fill login" + "message": "Autofill login" }, "autoFillCard": { - "message": "Auto-fill card" + "message": "Autofill card" }, "autoFillIdentity": { - "message": "Auto-fill identity" + "message": "Autofill identity" }, "generatePasswordCopied": { "message": "Generate password (copied)" @@ -150,7 +171,7 @@ "message": "Log in to your vault" }, "autoFillInfo": { - "message": "There are no logins available to auto-fill for the current browser tab." + "message": "There are no logins available to autofill for the current browser tab." }, "addLogin": { "message": "ავტორიზაციის დამატება" @@ -280,6 +301,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,16 +384,56 @@ "message": "Minimum password length" }, "uppercase": { - "message": "Uppercase (A-Z)" + "message": "Uppercase (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Lowercase (a-z)" + "message": "Lowercase (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numbers (0-9)" + "message": "Numbers (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Special characters (!@#$%^&*)" + "message": "Special characters (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "სიტყვათა რაოდენობა" @@ -376,7 +455,12 @@ "message": "Minimum special" }, "avoidAmbChar": { - "message": "Avoid ambiguous characters" + "message": "Avoid ambiguous characters", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Search vault" @@ -556,6 +640,18 @@ "security": { "message": "უსაფრთხოება" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "დაფიქსირდა შეცდომა" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "ერთჯერადი კოდი აუცილებელია." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -613,7 +718,7 @@ } }, "autofillError": { - "message": "Unable to auto-fill the selected item on this page. Copy and paste the information instead." + "message": "Unable to autofill the selected item on this page. Copy and paste the information instead." }, "totpCaptureError": { "message": "Unable to scan QR code from the current webpage" @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Your login session has expired." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "New URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "Item added" }, @@ -737,23 +873,32 @@ "enableAddLoginNotification": { "message": "Ask to add login" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "Ask to add an item if one isn't found in your vault." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Show cards on Tab page" }, "showCardsCurrentTabDesc": { - "message": "List card items on the Tab page for easy auto-fill." + "message": "List card items on the Tab page for easy autofill." + }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" }, "showIdentitiesCurrentTabDesc": { - "message": "List identity items on the Tab page for easy auto-fill." + "message": "List identity items on the Tab page for easy autofill." }, "clearClipboard": { "message": "Clear clipboard", @@ -791,7 +936,7 @@ "message": "Update" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Unlock" @@ -810,10 +955,10 @@ }, "defaultUriMatchDetection": { "message": "Default URI match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { - "message": "Choose the default way that URI match detection is handled for logins when performing actions such as auto-fill." + "message": "Choose the default way that URI match detection is handled for logins when performing actions such as autofill." }, "theme": { "message": "Theme" @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a Premium member!" }, "premiumCurrentMemberThanks": { "message": "Thank you for supporting Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "All for just $PRICE$ /year!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Refresh complete" }, @@ -1028,7 +1191,7 @@ "message": "Copy TOTP automatically" }, "disableAutoTotpCopyDesc": { - "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login." + "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you autofill the login." }, "enableAutoBiometricsPrompt": { "message": "Ask for biometrics on launch" @@ -1178,14 +1341,23 @@ "message": "Environment URLs saved" }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,38 +1371,57 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "enableAutoFillOnPageLoadDesc": { - "message": "If a login form is detected, auto-fill when the web page loads." + "message": "If a login form is detected, autofill when the web page loads." + }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Compromised or untrusted websites can exploit autofill on page load." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" }, "learnMoreAboutAutofill": { - "message": "Learn more about auto-fill" + "message": "Learn more about autofill" }, "defaultAutoFillOnPageLoad": { "message": "Default autofill setting for login items" }, "defaultAutoFillOnPageLoadDesc": { - "message": "You can turn off auto-fill on page load for individual login items from the item's Edit view." + "message": "You can turn off autofill on page load for individual login items from the item's Edit view." }, "itemAutoFillOnPageLoad": { - "message": "Auto-fill on page load (if set up in Options)" + "message": "Autofill on page load (if set up in Options)" }, "autoFillOnPageLoadUseDefault": { "message": "Use default setting" }, "autoFillOnPageLoadYes": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "autoFillOnPageLoadNo": { - "message": "Do not auto-fill on page load" + "message": "Do not autofill on page load" }, "commandOpenPopup": { "message": "Open vault popup" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Open vault in sidebar" }, - "commandAutofillDesc": { - "message": "Auto-fill the last used login for the current website" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Generate and copy a new random password to the clipboard" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Linked", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -1533,6 +1742,10 @@ "message": "Base domain", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domain name", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Match detection", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Default match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Toggle options" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "There are no passwords to list." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Remove" }, @@ -1677,6 +1899,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" }, @@ -1716,16 +1961,16 @@ "message": "Timeout action confirmation" }, "autoFillAndSave": { - "message": "Auto-fill and save" + "message": "Autofill and save" }, "fillAndSave": { "message": "Fill and save" }, "autoFillSuccessAndSavedUri": { - "message": "Item auto-filled and URI saved" + "message": "Item autofilled and URI saved" }, "autoFillSuccess": { - "message": "Item auto-filled " + "message": "Item autofilled " }, "insecurePageWarning": { "message": "Warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page." @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ is not a valid domain", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Password protected" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Copy Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send created", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2606,10 +2906,10 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { - "message": "How to auto-fill" + "message": "How to autofill" }, "autofillSelectInfoWithCommand": { "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", @@ -2627,16 +2927,25 @@ "message": "Got it" }, "autofillSettings": { - "message": "Auto-fill settings" + "message": "Autofill settings" + }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 95ec45da720..338d70eaf58 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "Create account" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "Log in" - }, "enterpriseSingleSignOn": { "message": "Enterprise single sign-on" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Master password hint (optional)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "Tab" }, @@ -107,17 +113,32 @@ "copySecurityCode": { "message": "Copy security code" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { - "message": "Auto-fill" + "message": "Autofill" }, "autoFillLogin": { - "message": "Auto-fill login" + "message": "Autofill login" }, "autoFillCard": { - "message": "Auto-fill card" + "message": "Autofill card" }, "autoFillIdentity": { - "message": "Auto-fill identity" + "message": "Autofill identity" }, "generatePasswordCopied": { "message": "Generate password (copied)" @@ -150,7 +171,7 @@ "message": "Log in to your vault" }, "autoFillInfo": { - "message": "There are no logins available to auto-fill for the current browser tab." + "message": "There are no logins available to autofill for the current browser tab." }, "addLogin": { "message": "Add a login" @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "Minimum password length" }, "uppercase": { - "message": "Uppercase (A-Z)" + "message": "Uppercase (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Lowercase (a-z)" + "message": "Lowercase (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numbers (0-9)" + "message": "Numbers (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Special characters (!@#$%^&*)" + "message": "Special characters (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Number of words" @@ -376,7 +455,12 @@ "message": "Minimum special" }, "avoidAmbChar": { - "message": "Avoid ambiguous characters" + "message": "Avoid ambiguous characters", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Search vault" @@ -556,6 +640,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -613,7 +718,7 @@ } }, "autofillError": { - "message": "Unable to auto-fill the selected item on this page. Copy and paste the information instead." + "message": "Unable to autofill the selected item on this page. Copy and paste the information instead." }, "totpCaptureError": { "message": "Unable to scan QR code from the current webpage" @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Your login session has expired." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "New URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "Item added" }, @@ -737,23 +873,32 @@ "enableAddLoginNotification": { "message": "Ask to add login" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "Ask to add an item if one isn't found in your vault." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Show cards on Tab page" }, "showCardsCurrentTabDesc": { - "message": "List card items on the Tab page for easy auto-fill." + "message": "List card items on the Tab page for easy autofill." + }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" }, "showIdentitiesCurrentTabDesc": { - "message": "List identity items on the Tab page for easy auto-fill." + "message": "List identity items on the Tab page for easy autofill." }, "clearClipboard": { "message": "Clear clipboard", @@ -791,7 +936,7 @@ "message": "Update" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Unlock" @@ -810,10 +955,10 @@ }, "defaultUriMatchDetection": { "message": "Default URI match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { - "message": "Choose the default way that URI match detection is handled for logins when performing actions such as auto-fill." + "message": "Choose the default way that URI match detection is handled for logins when performing actions such as autofill." }, "theme": { "message": "Theme" @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a Premium member!" }, "premiumCurrentMemberThanks": { "message": "Thank you for supporting Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "All for just $PRICE$ /year!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Refresh complete" }, @@ -1028,7 +1191,7 @@ "message": "Copy TOTP automatically" }, "disableAutoTotpCopyDesc": { - "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login." + "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you autofill the login." }, "enableAutoBiometricsPrompt": { "message": "Ask for biometrics on launch" @@ -1178,14 +1341,23 @@ "message": "Environment URLs saved" }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,38 +1371,57 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "enableAutoFillOnPageLoadDesc": { - "message": "If a login form is detected, auto-fill when the web page loads." + "message": "If a login form is detected, autofill when the web page loads." + }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Compromised or untrusted websites can exploit autofill on page load." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" }, "learnMoreAboutAutofill": { - "message": "Learn more about auto-fill" + "message": "Learn more about autofill" }, "defaultAutoFillOnPageLoad": { "message": "Default autofill setting for login items" }, "defaultAutoFillOnPageLoadDesc": { - "message": "You can turn off auto-fill on page load for individual login items from the item's Edit view." + "message": "You can turn off autofill on page load for individual login items from the item's Edit view." }, "itemAutoFillOnPageLoad": { - "message": "Auto-fill on page load (if set up in Options)" + "message": "Autofill on page load (if set up in Options)" }, "autoFillOnPageLoadUseDefault": { "message": "Use default setting" }, "autoFillOnPageLoadYes": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "autoFillOnPageLoadNo": { - "message": "Do not auto-fill on page load" + "message": "Do not autofill on page load" }, "commandOpenPopup": { "message": "Open vault popup" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Open vault in sidebar" }, - "commandAutofillDesc": { - "message": "Auto-fill the last used login for the current website" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Generate and copy a new random password to the clipboard" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Linked", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -1533,6 +1742,10 @@ "message": "Base domain", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domain name", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Match detection", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Default match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Toggle options" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "There are no passwords to list." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Remove" }, @@ -1677,6 +1899,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" }, @@ -1716,16 +1961,16 @@ "message": "Timeout action confirmation" }, "autoFillAndSave": { - "message": "Auto-fill and save" + "message": "Autofill and save" }, "fillAndSave": { "message": "Fill and save" }, "autoFillSuccessAndSavedUri": { - "message": "Item auto-filled and URI saved" + "message": "Item autofilled and URI saved" }, "autoFillSuccess": { - "message": "Item auto-filled " + "message": "Item autofilled " }, "insecurePageWarning": { "message": "Warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page." @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ is not a valid domain", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Password protected" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Copy Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send created", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2606,10 +2906,10 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { - "message": "How to auto-fill" + "message": "How to autofill" }, "autofillSelectInfoWithCommand": { "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", @@ -2627,16 +2927,25 @@ "message": "Got it" }, "autofillSettings": { - "message": "Auto-fill settings" + "message": "Autofill settings" + }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 8786927c40c..aa902ec5d7e 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "ನಿಮ್ಮ ಸುರಕ್ಷಿತ ವಾಲ್ಟ್ ಅನ್ನು ಪ್ರವೇಶಿಸಲು ಲಾಗ್ ಇನ್ ಮಾಡಿ ಅಥವಾ ಹೊಸ ಖಾತೆಯನ್ನು ರಚಿಸಿ." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "ಖಾತೆ ತೆರೆ" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "ಲಾಗಿನ್" - }, "enterpriseSingleSignOn": { "message": "ಎಂಟರ್‌ಪ್ರೈಸ್ ಏಕ ಸೈನ್-ಆನ್" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "ಮಾಸ್ಟರ್ ಪಾಸ್ವರ್ಡ್ ಸುಳಿವು (ಐಚ್ಛಿಕ)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "ಟ್ಯಾಬ್" }, @@ -107,17 +113,32 @@ "copySecurityCode": { "message": "ಭದ್ರತಾ ಕೋಡ್ ಅನ್ನು ನಕಲಿಸಿ" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "ಸ್ವಯಂ ಭರ್ತಿ" }, "autoFillLogin": { - "message": "Auto-fill login" + "message": "Autofill login" }, "autoFillCard": { - "message": "Auto-fill card" + "message": "Autofill card" }, "autoFillIdentity": { - "message": "Auto-fill identity" + "message": "Autofill identity" }, "generatePasswordCopied": { "message": "ಪಾಸ್ವರ್ಡ್ ರಚಿಸಿ (ನಕಲಿಸಲಾಗಿದೆ)" @@ -280,6 +301,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,16 +384,56 @@ "message": "Minimum password length" }, "uppercase": { - "message": "Uppercase (A-Z)" + "message": "Uppercase (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Lowercase (a-z)" + "message": "Lowercase (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numbers (0-9)" + "message": "Numbers (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Special characters (!@#$%^&*)" + "message": "Special characters (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "ಪದಗಳ ಸಂಖ್ಯೆ" @@ -376,7 +455,12 @@ "message": "ಕನಿಷ್ಠ ವಿಶೇಷ" }, "avoidAmbChar": { - "message": "ಅಸ್ಪಷ್ಟ ಅಕ್ಷರಗಳನ್ನು ತಪ್ಪಿಸಿ" + "message": "ಅಸ್ಪಷ್ಟ ಅಕ್ಷರಗಳನ್ನು ತಪ್ಪಿಸಿ", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "ವಾಲ್ಟ್ ಹುಡುಕಿ" @@ -556,6 +640,18 @@ "security": { "message": "ಭದ್ರತೆ" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "ದೋಷ ಸಂಭವಿಸಿದೆ" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "ನಿಮ್ಮ ಹೊಸ ಖಾತೆಯನ್ನು ರಚಿಸಲಾಗಿದೆ! ನೀವು ಈಗ ಲಾಗ್ ಇನ್ ಮಾಡಬಹುದು." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "ಪರಿಶೀಲನೆ ಕೋಡ್ ಅಗತ್ಯವಿದೆ." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "ನಿಮ್ಮ ಲಾಗಿನ್ ಸೆಷನ್ ಅವಧಿ ಮೀರಿದೆ." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "ಲಾಗ್ ಔಟ್ ಮಾಡಲು ನೀವು ಖಚಿತವಾಗಿ ಬಯಸುವಿರಾ?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "ಹೊಸ ಯುಆರ್ಐ" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "ಐಟಂ ಸೇರಿಸಲಾಗಿದೆ" }, @@ -737,23 +873,32 @@ "enableAddLoginNotification": { "message": "Ask to add login" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "\"ಲಾಗಿನ್ ಅಧಿಸೂಚನೆಯನ್ನು ಸೇರಿಸಿ\" ನೀವು ಮೊದಲ ಬಾರಿಗೆ ಪ್ರವೇಶಿಸಿದಾಗಲೆಲ್ಲಾ ಹೊಸ ಲಾಗಿನ್‌ಗಳನ್ನು ನಿಮ್ಮ ವಾಲ್ಟ್‌ಗೆ ಉಳಿಸಲು ಸ್ವಯಂಚಾಲಿತವಾಗಿ ಕೇಳುತ್ತದೆ." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Show cards on Tab page" }, "showCardsCurrentTabDesc": { - "message": "List card items on the Tab page for easy auto-fill." + "message": "List card items on the Tab page for easy autofill." + }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" }, "showIdentitiesCurrentTabDesc": { - "message": "List identity items on the Tab page for easy auto-fill." + "message": "List identity items on the Tab page for easy autofill." }, "clearClipboard": { "message": "ಕ್ಲಿಪ್‌ಬೋರ್ಡ್ ತೆರವುಗೊಳಿಸಿ", @@ -791,7 +936,7 @@ "message": "ಹೌದು, ಈಗ ನವೀಕರಿಸಿ" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Unlock" @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "ಡೀಫಾಲ್ಟ್ ಯುಆರ್ಐ ಹೊಂದಾಣಿಕೆ ಪತ್ತೆ", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "ಸ್ವಯಂ ಭರ್ತಿಯಂತಹ ಕ್ರಿಯೆಗಳನ್ನು ನಿರ್ವಹಿಸುವಾಗ ಲಾಗಿನ್‌ಗಳಿಗಾಗಿ URI ಹೊಂದಾಣಿಕೆ ಪತ್ತೆಹಚ್ಚುವಿಕೆಯನ್ನು ನಿರ್ವಹಿಸುವ ಪೂರ್ವನಿಯೋಜಿತ ಮಾರ್ಗವನ್ನು ಆರಿಸಿ." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "ಫೈಲ್ ಲಗತ್ತುಗಳಿಗಾಗಿ 1 ಜಿಬಿ ಎನ್‌ಕ್ರಿಪ್ಟ್ ಮಾಡಿದ ಸಂಗ್ರಹ." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "ನೀವು ಬಿಟ್ವಾರ್ಡೆನ್.ಕಾಮ್ ವೆಬ್ ವಾಲ್ಟ್ನಲ್ಲಿ ಪ್ರೀಮಿಯಂ ಸದಸ್ಯತ್ವವನ್ನು ಖರೀದಿಸಬಹುದು. ನೀವು ಈಗ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಭೇಟಿ ನೀಡಲು ಬಯಸುವಿರಾ?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "ನೀವು ಪ್ರೀಮಿಯಂ ಸದಸ್ಯರಾಗಿದ್ದೀರಿ!" }, "premiumCurrentMemberThanks": { "message": "ಬಿಟ್ವಾರ್ಡೆನ್ ಅವರನ್ನು ಬೆಂಬಲಿಸಿದ್ದಕ್ಕಾಗಿ ಧನ್ಯವಾದಗಳು." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "ಎಲ್ಲವೂ ಕೇವಲ $PRICE$ / ವರ್ಷಕ್ಕೆ!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "ರಿಫ್ರೆಶ್ ಪೂರ್ಣಗೊಂಡಿದೆ" }, @@ -1178,14 +1341,23 @@ "message": "ಪರಿಸರ URL ಗಳನ್ನು ಉಳಿಸಲಾಗಿದೆ." }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,20 +1371,39 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "ಪುಟ ಲೋಡ್‌ನಲ್ಲಿ ಸ್ವಯಂ ಭರ್ತಿ ಸಕ್ರಿಯಗೊಳಿಸಿ" }, "enableAutoFillOnPageLoadDesc": { "message": "ಲಾಗಿನ್ ಫಾರ್ಮ್ ಪತ್ತೆಯಾದಲ್ಲಿ, ವೆಬ್ ಪುಟ ಲೋಡ್ ಆಗುವಾಗ ಸ್ವಯಂಚಾಲಿತವಾಗಿ ಸ್ವಯಂ ಭರ್ತಿ ಮಾಡಿ." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Compromised or untrusted websites can exploit autofill on page load." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" }, "learnMoreAboutAutofill": { - "message": "Learn more about auto-fill" + "message": "Learn more about autofill" }, "defaultAutoFillOnPageLoad": { "message": "ಲಾಗಿನ್ ಐಟಂಗಳಿಗಾಗಿ ಡೀಫಾಲ್ಟ್ ಆಟೋಫಿಲ್ ಸೆಟ್ಟಿಂಗ್" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "ಸೈಡ್ಬಾರ್ನಲ್ಲಿ ವಾಲ್ಟ್ ತೆರೆಯಿರಿ" }, - "commandAutofillDesc": { - "message": "ಪ್ರಸ್ತುತ ವೆಬ್‌ಸೈಟ್‌ಗಾಗಿ ಕೊನೆಯದಾಗಿ ಬಳಸಿದ ಲಾಗಿನ್ ಅನ್ನು ಸ್ವಯಂ ಭರ್ತಿ ಮಾಡಿ" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "ಕ್ಲಿಪ್ಬೋರ್ಡ್ಗೆ ಹೊಸ ಯಾದೃಚ್ಛಿಕ ಪಾಸ್ವರ್ಡ್ ಅನ್ನು ರಚಿಸಿ ಮತ್ತು ನಕಲಿಸಿ" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "ಬೂಲಿಯನ್" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Linked", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "ಪಾಸ್ವರ್ಡ್ ಇತಿಹಾಸ" }, @@ -1533,6 +1742,10 @@ "message": "ಮೂಲ ಡೊಮೇನ್", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domain name", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "ಹೊಂದಾಣಿಕೆ ಪತ್ತೆ", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "ಡೀಫಾಲ್ಟ್ ಪಂದ್ಯ ಪತ್ತೆ", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "ಟಾಗಲ್ ಆಯ್ಕೆಗಳು" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "ಪಟ್ಟಿ ಮಾಡಲು ಯಾವುದೇ ಪಾಸ್ವರ್ಡ್ಗಳು ಇಲ್ಲ." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "ತೆಗೆ" }, @@ -1677,6 +1899,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": "ವಾಲ್ಟ್ ಸಮಯ ಮೀರುವ ಕ್ರಿಯೆ" }, @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "ನಿಮ್ಮ ಹೊಸ ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್ ನೀತಿಯ ಅವಶ್ಯಕತೆಗಳನ್ನು ಪೂರೈಸುವುದಿಲ್ಲ." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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": "ಬಯೊಮಿಟ್ರಿಕ್ಸ್ ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಲಾಗಿಲ್ಲ" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "ಹೊರತುಪಡಿಸಿದ ಡೊಮೇನ್ಗಳು" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ ಮಾನ್ಯವಾದ ಡೊಮೇನ್ ಅಲ್ಲ", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "ಕಳುಹಿಸಿ", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "ಪಾಸ್ವರ್ಡ್ ರಕ್ಷಿತ" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "ಲಿಂಕ್ ಕಳುಹಿಸಿ", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "ಕಳುಹಿಸು ರಚಿಸಲಾಗಿದೆ", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "ಕಳುಹಿಸಿದ ಸಂಪಾದನೆ", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "ಇಮೇಲ್ ಪರಿಶೀಲನೆ ಅಗತ್ಯವಿದೆ" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "ಈ ವೈಶಿಷ್ಟ್ಯವನ್ನು ಬಳಸಲು ನಿಮ್ಮ ಇಮೇಲ್ ಅನ್ನು ನೀವು ಪರಿಶೀಲಿಸಬೇಕು. ವೆಬ್ ವಾಲ್ಟ್ನಲ್ಲಿ ನಿಮ್ಮ ಇಮೇಲ್ ಅನ್ನು ನೀವು ಪರಿಶೀಲಿಸಬಹುದು." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2606,10 +2906,10 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { - "message": "How to auto-fill" + "message": "How to autofill" }, "autofillSelectInfoWithCommand": { "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", @@ -2627,16 +2927,25 @@ "message": "Got it" }, "autofillSettings": { - "message": "Auto-fill settings" + "message": "Autofill settings" + }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index 47c593dee21..cd2fff44a41 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "안전 보관함에 접근하려면 로그인하거나 새 계정을 만드세요." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "계정 만들기" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "로그인" - }, "enterpriseSingleSignOn": { "message": "엔터프라이즈 통합 인증 (SSO)" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "마스터 비밀번호 힌트 (선택)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "탭" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "보안 코드 복사" }, + "copyName": { + "message": "이름 복사" + }, + "copyCompany": { + "message": "회사 복사" + }, + "copySSN": { + "message": "주민등록번호 복사" + }, + "copyPassportNumber": { + "message": "여권 번호 복사" + }, + "copyLicenseNumber": { + "message": "운전면허 번호 복사" + }, "autoFill": { "message": "자동 완성" }, @@ -233,7 +254,7 @@ "message": "More from Bitwarden" }, "continueToBitwardenDotCom": { - "message": "Continue to bitwarden.com?" + "message": "bitwarden.com 으로 이동할까요?" }, "bitwardenForBusiness": { "message": "Bitwarden for Business" @@ -280,6 +301,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,16 +384,56 @@ "message": "최소 비밀번호 길이" }, "uppercase": { - "message": "대문자 (A-Z)" + "message": "대문자 (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "소문자 (a-z)" + "message": "소문자 (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "숫자 (0-9)" + "message": "숫자 (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "특수 문자 (!@#$%^&*)" + "message": "특수 문자 (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "단어 수" @@ -376,7 +455,12 @@ "message": "특수 문자 최소 개수" }, "avoidAmbChar": { - "message": "모호한 문자 사용 안 함" + "message": "모호한 문자 사용 안 함", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "보관함 검색" @@ -556,6 +640,18 @@ "security": { "message": "보안" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "오류가 발생했습니다" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "계정 생성이 완료되었습니다! 이제 로그인하실 수 있습니다." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "로그인에 성공했습니다." }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "인증 코드는 반드시 입력해야 합니다." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "유효하지 않은 확인 코드" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "현재 웹페이지에서 QR 코드 스캔하기" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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": "인증서 키 (TOTP) 복사" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "로그인 세션이 만료되었습니다." }, + "logIn": { + "message": "로그인" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "만료된 링크" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "정말 로그아웃하시겠습니까?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "새 URI" }, + "addDomain": { + "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": { "message": "항목 추가함" }, @@ -737,23 +873,32 @@ "enableAddLoginNotification": { "message": "로그인을 추가할 건지 물어보기" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "\"로그인 추가 알림\"을 사용하면 새 로그인을 사용할 때마다 보관함에 그 로그인을 추가할 것인지 물어봅니다." }, "addLoginNotificationDescAlt": { "message": "보관함에 항목이 없을 경우 추가하라는 메시지를 표시합니다. 모든 로그인된 계정에 적용됩니다." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "탭 페이지에 카드 표시" }, "showCardsCurrentTabDesc": { - "message": "List card items on the Tab page for easy auto-fill." + "message": "List card items on the Tab page for easy autofill." + }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" }, "showIdentitiesCurrentTabDesc": { - "message": "List identity items on the Tab page for easy auto-fill." + "message": "List identity items on the Tab page for easy autofill." }, "clearClipboard": { "message": "클립보드 비우기", @@ -791,7 +936,7 @@ "message": "예, 지금 변경하겠습니다." }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "잠금 해제" @@ -810,10 +955,10 @@ }, "defaultUriMatchDetection": { "message": "기본 URI 일치 인식", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { - "message": "자동 완성 등의 작업에서 각 로그인 항목의 URI 일치 감지를 처리할 기본 방법을 선택하세요." + "message": "자동 완성같은 작업을 수행할 때 로그인에 대해 URI 일치 감지가 처리되는 기본 방법을 선택하십시오." }, "theme": { "message": "테마" @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1GB의 암호화된 파일 저장소." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "bitwarden.com 웹 보관함에서 프리미엄 멤버십을 구입할 수 있습니다. 지금 웹 사이트를 방문하시겠습니까?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "프리미엄 사용자입니다!" }, "premiumCurrentMemberThanks": { "message": "Bitwarden을 지원해 주셔서 감사합니다." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "이 모든 기능을 연 $PRICE$에 이용하실 수 있습니다!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "새로 고침 완료" }, @@ -1179,10 +1342,19 @@ }, "showAutoFillMenuOnFormFields": { "message": "입력 필드에 자동 완성 메뉴 표시", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { - "message": "모든 로그인된 계정에 적용됩니다." + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { + "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { "message": "충돌을 방지하기 위해 브라우저의 기본 암호 관리 설정을 해제합니다." @@ -1202,17 +1374,36 @@ "message": "자동 완성 아이콘이 선택되었을 때", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "페이지 로드 시 자동 완성 사용" }, "enableAutoFillOnPageLoadDesc": { "message": "로그인 양식을 감지하면 웹 페이지 로드 시 자동 완성을 자동으로 수행합니다." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "취약하거나 신뢰할 수 없는 웹사이트 페이지 로드 시 자동 완성이 악용될 수 있습니다." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" + }, "learnMoreAboutAutofill": { - "message": "Learn more about auto-fill" + "message": "Learn more about autofill" }, "defaultAutoFillOnPageLoad": { "message": "로그인 항목에 대한 기본 자동 완성 설정" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "사이드바에서 보관함 열기" }, - "commandAutofillDesc": { - "message": "현재 웹사이트에서 최근에 사용했던 로그인을 자동 완성합니다." + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "새 무작위 비밀번호를 만들고 클립보드에 복사합니다." @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "참 / 거짓" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "연결됨", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "비밀번호 변경 기록" }, @@ -1533,6 +1742,10 @@ "message": "기본 도메인", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "도메인 이름", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "일치 인식", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "기본 일치 인식", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "표시 / 숨기기" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "비밀번호가 없습니다." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "제거" }, @@ -1677,6 +1899,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": "보관함 시간 제한 초과시 동작" }, @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "새 마스터 비밀번호가 정책 요구 사항을 따르지 않습니다." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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": "생체 인식이 활성화되지 않음" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "제외된 도메인" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ 도메인은 유효한 도메인이 아닙니다.", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "비밀번호로 보호됨" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Send 링크 복사", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send 생성함", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send 수정함", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "이메일 인증 필요함" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "이 기능을 사용하려면 이메일 인증이 필요합니다. 웹 보관함에서 이메일을 인증할 수 있습니다." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "자동 등록" }, @@ -2606,10 +2906,10 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { - "message": "How to auto-fill" + "message": "How to autofill" }, "autofillSelectInfoWithCommand": { "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "자동 완성 설정" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" + }, "autofillShortcut": { "message": "자동 완성 키보드 단축키" }, - "autofillShortcutNotSet": { - "message": "자동 완성 단축키가 설정되지 않았습니다. 브라우저 설정에서 단축키를 변경하세요." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "계정 잠금 해제", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,8 +3486,8 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "사이트에서 인증을 요구합니다. 이 기능은 비밀번호가 없는 계정에서는 아직 지원하지 않습니다." }, - "logInWithPasskey": { - "message": "패스키로 로그인하시겠어요?" + "logInWithPasskeyQuestion": { + "message": "Log in with passkey?" }, "passkeyAlreadyExists": { "message": "이미 이 애플리케이션에 해당하는 패스키가 있습니다." @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "사이트와 일치하는 로그인이 없습니다." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,8 +3510,11 @@ "savePasskeyNewLogin": { "message": "새 로그인으로 패스키 저장" }, - "choosePasskey": { - "message": "패스키를 저장할 로그인 선택하기" + "chooseCipherForPasskeySave": { + "message": "Choose a login to save this passkey to" + }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" }, "passkeyItem": { "message": "패스키 항목" @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Bitwarden을 기본 비밀번호 관리자로 지정하시겠습니까?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "패스키 제거됨" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 4686dacb726..c60b8b7b211 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Prisijunkite arba sukurkite naują paskyrą, kad galėtumėte pasiekti saugyklą." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "Sukurti paskyrą" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Baigkite kurti paskyrą nustatydami slaptažodį" }, - "login": { - "message": "Prisijungti" - }, "enterpriseSingleSignOn": { "message": "Vienkartinis įmonės prisijungimas" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Pagrindinio slaptažodžio užuomina (neprivaloma)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "Skirtukas" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Kopijuoti saugos kodą" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "Automatinis užpildymas" }, @@ -280,6 +301,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ą" }, @@ -345,16 +384,56 @@ "message": "Minimalus slaptažodžio ilgis" }, "uppercase": { - "message": "Didžiosiomis (A-Z)" + "message": "Didžiosiomis (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Mažosiomis (a-z)" + "message": "Mažosiomis (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Skaitmenys (0-9)" + "message": "Skaitmenys (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Specialieji simboliai (!@#$%^&*)" + "message": "Specialieji simboliai (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Žodžių skaičius" @@ -376,7 +455,12 @@ "message": "Mažiausiai simbolių" }, "avoidAmbChar": { - "message": "Vengti dviprasmiškų simbolių" + "message": "Vengti dviprasmiškų simbolių", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Ieškoti saugykloje" @@ -556,6 +640,18 @@ "security": { "message": "Apsauga" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Įvyko klaida" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Jūsų paskyra sukurta! Galite prisijungti." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "Jūs sėkmingai prisijungėte" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Būtinas patvirtinimo kodas." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Neteisingas patvirtinimo kodas" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Nuskaitykite autentifikatoriaus QR kodą iš dabartinio tinklalapio" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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": "Kopijuoti Autentifikatoriaus raktą (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Sesijos laikas baigėsi." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Ar tikrai norite atsijungti?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Naujas URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "Pridėtas elementas" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Prašyti pridėti prisijungimą" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "Prisijungimo pridėjimo pranešimas automatiškai Jūs paragina išsaugoti naujus prisijungimus Jūsų saugykloje, kuomet prisijungiate pirmą kartą." }, "addLoginNotificationDescAlt": { "message": "Paprašykite pridėti elementą, jei jo nerasta Jūsų saugykloje. Taikoma visoms prisijungusioms paskyroms." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Rodyti korteles skirtuko puslapyje" }, "showCardsCurrentTabDesc": { "message": "Pateikti kortelių elementų skirtuko puslapyje sąrašą, kad būtų lengva automatiškai užpildyti." }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" + }, "showIdentitiesCurrentTab": { "message": "Rodyti tapatybes skirtuko puslapyje" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Numatytojo URI atitikimo aptikimas", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Pasirinkite standartinį būdą, kuriuo URI būtų aptinkamas prisijungiant, kuomet yra naudojamas automatinio užpildymo veiksmas." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB užšifruotos vietos diske bylų prisegimams." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Patentuotos dviejų žingsnių prisijungimo parinktys, tokios kaip YubiKey ir Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Galite įsigyti „Premium“ narystę „bitwarden.com“ žiniatinklio saugykloje. Ar norite apsilankyti svetainėje dabar?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Tu esi Premium narys!" }, "premiumCurrentMemberThanks": { "message": "Dėkojame, kad palaikote „Bitwarden“." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "Visa tai tik už $PRICE$ / metus!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Atnaujinimas įvykdytas" }, @@ -1179,10 +1342,19 @@ }, "showAutoFillMenuOnFormFields": { "message": "Rodyti automatinio pildymo meniu formos laukeliuose", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { - "message": "Taikoma visoms prisijungusioms paskyroms." + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { + "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { "message": "Išjunkite naršyklėje integruotus slaptažodžių tvarkyklės nustatymus, kad išvengtumėte konfliktų." @@ -1202,15 +1374,34 @@ "message": "Kai pasirinkta automatinio užpildymo piktograma", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "Automatiškai užpildyti užsikrovus puslapiui" }, "enableAutoFillOnPageLoadDesc": { "message": "Jei aptikta prisijungimo forma, automatiškai užpildyti, kai kraunamas tinklalapis." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Pažeistos arba nepatikimos svetainės gali išnaudoti automatinį užpildymą įkeliant puslapį." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" + }, "learnMoreAboutAutofill": { "message": "Sužinokite daugiau apie automatinį užpildymą" }, @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Atidaryti saugyklą šoninėje juostoje" }, - "commandAutofillDesc": { - "message": "Automatiškai užpildykite paskutinį naudotą dabartinės svetainės prisijungimą" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Sugeneruokite ir nukopijuokite naują atsitiktinį slaptažodį į iškarpinę" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Taip/Ne" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Susieta", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Slaptažodžio istorija" }, @@ -1533,6 +1742,10 @@ "message": "Bazinis domenas", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domenas", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Atitikmens aptikimas", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Numatytasis atitikties aptikimas", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Perjungti opcijas" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Slaptažodžių sąraše nėra." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Pašalinti" }, @@ -1677,6 +1899,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" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Tavo naujasis pagrindinis slaptažodis neatitinka politikos reikalavimų." }, - "receiveMarketingEmails": { - "message": "Gaukite „Bitwarden“ el. laiškus su skelbimais, patarimais ir tyrimų galimybėmis." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Atsisakyti prenumeratos" @@ -1874,6 +2119,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ų" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Atrakinkite šį naudotoją darbalaukio programoje ir bandykite dar kartą." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrika nepavyko" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "Organizacijos politika blokavo elementų importavimą į Jūsų individualią saugyklą." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Išskirti domenai" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "„Bitwarden“ neprašys išsaugoti prisijungimo detalių šiems domenams, visose prisijungusiose paskyrose. Turite atnaujinti puslapį, kad pokyčiai pradėtų galioti." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ yra klaidingas domenas", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Siųsti", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Apsaugota slaptažodžiu" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Kopijuoti siuntimo nuorodą", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Siuntinys sukurtas", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Siuntinys išsaugotas", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Reikalingas elektroninio pašto patvirtinimas" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Turite patvirtinti savo el. paštą, kad galėtumėte naudotis šia funkcija. Savo el. pašto adresą galite patvirtinti žiniatinklio saugykloje." }, @@ -2179,6 +2476,9 @@ "updateWeakMasterPasswordWarning": { "message": "Jūsų pagrindinis slaptažodis neatitinka vieno ar kelių organizacijos slaptažodžiui keliamų reikalavimų. Privalote atnaujinti pagrindinį slaptažodį, kad galėtumėte pasiekti saugyklą. Tęsdami būsite atjungtas nuo dabartinės savo sesijos, todėl turėsite vėl prisijungti. 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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatinis registravimas" }, @@ -2606,7 +2906,7 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { "message": "Kaip automatiškai užpildyti" @@ -2627,16 +2927,25 @@ "message": "Got it" }, "autofillSettings": { - "message": "Auto-fill settings" + "message": "Autofill settings" + }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Patikimas įrenginys" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Elementai su pagrindinio slaptažodžio raginimu negali būti automatiškai užpildyti įkeliant puslapį. Automatinis pildymas, įkeliant puslapį, išjungtas.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "Automatinis pildymas įkeliant puslapį nustatytas naudoti numatytąjį nustatymą.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Išjunkite pagrindinio slaptažodžio raginimą, jei norite redaguoti šį lauką", @@ -2911,10 +3240,18 @@ "message": "Atrakinkite savo paskyrą, kad pamatytumėte atitinkamus prisijungimus", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Atrakinti paskyrą", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Užpildykite prisijungimo duomenis", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Pridėti naują saugyklos elementą", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Galimas „Bitwarden“ automatinio pildymo meniu. Norėdami pasirinkti, paspauskite rodyklės žemyn klavišą.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Paleisk DUO ir sek veiksmus, kad baigtum prisijungti." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Netinkamas failo slaptažodis, prašome naudoti slaptažodį, kurį įvedėte kurdami eksportuojamą failą." }, - "importDestination": { - "message": "Importavimo paskirties vieta" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Sužinoti apie importavimo parinktis" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,6 +3659,30 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Padaryti Bitwarden savo numatytuoju slaptažodžių tvarkytuvu?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" @@ -3317,10 +3711,18 @@ "message": "Kredencialai išsaugoti sėkmingai.", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Kredencialai atnaujinti sėkmingai.", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Klaida išsaugant kredencialus. Išsamesnės informacijos patikrink konsolėje.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Pašalintas slaptaraktis" }, - "unassignedItemsBannerNotice": { - "message": "Pranešimas: nepriskirti organizacijos elementai nebėra matomi peržiūros rodinyje Visi saugyklos ir yra pasiekiami tik per Administratoriaus konsolę." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Pranešimas: 2024 m. gegužės 16 d. nepriskirti organizacijos elementai nebėra matomi peržiūros rodinyje Visi saugyklos ir yra pasiekiami tik per Administratoriaus konsolę." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Priskirkite šiuos elementus kolekcijai iš", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ", kad jie būtų matomi.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3493,13 +3881,13 @@ "message": "Items with no folder" }, "itemDetails": { - "message": "Item details" + "message": "Elemento informacija" }, "itemName": { - "message": "Item name" + "message": "Elemento pavadinimas" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Negalite pašalinti kolekcijų su Peržiūrėti tik leidimus: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3899,33 @@ "message": "Organization is deactivated" }, "owner": { - "message": "Owner" + "message": "Savininkas" }, "selfOwnershipLabel": { - "message": "You", + "message": "Jūs", "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." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Įkelti" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filtrai" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Kortelės duomenys" + }, + "cardBrandDetails": { + "message": "„$BRAND$“ duomenys", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Pridėti paskyrą" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 7643d6b721e..2f1f123020d 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Jāpiesakās vai jāizveido jauns konts, lai piekļūtu drošajai glabātavai." }, + "inviteAccepted": { + "message": "Uzaicinājums apstiprināts" + }, "createAccount": { "message": "Izveidot kontu" }, @@ -20,10 +23,7 @@ "message": "Jāiestata droša parole" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Jāpabeidz sava konta izveida ar paroles iestatīšanu" - }, - "login": { - "message": "Pieteikties" + "message": "Sava konta izveidošana jāpabeidz ar paroles iestatīšanu" }, "enterpriseSingleSignOn": { "message": "Uzņēmuma vienotā pieteikšanās" @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Galvenās paroles norāde (nav nepieciešama)" }, + "joinOrganization": { + "message": "Pievienoties apvienībai" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Pabeigt pievienošanos šai apvienībai ar galvenās paroles iestatīšanu." + }, "tab": { "message": "Cilne" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Ievietot drošības kodu starpliktuvē" }, + "copyName": { + "message": "Ievietot nosaukumu starpliktuvē" + }, + "copyCompany": { + "message": "Ievietot uzņēmumu starpliktuvē" + }, + "copySSN": { + "message": "Ievietot sociālās nodrošināšanas numuru starpliktuvē" + }, + "copyPassportNumber": { + "message": "Ievietot pases numuru starpliktuvē" + }, + "copyLicenseNumber": { + "message": "Ievietot licences numuru starpliktuvē" + }, "autoFill": { "message": "Automātiskā aizpilde" }, @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "Mazākais pieļaujamais paroles garums" }, "uppercase": { - "message": "Lielie burti (A-Z)" + "message": "Lielie burti (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Mazie burti (a-z)" + "message": "Mazie burti (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Cipari (0-9)" + "message": "Cipari (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Īpašās rakstzīmes (!@#$%^&*)" + "message": "Īpašās rakstzīmes (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Iekļaut", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Iekļaut lielos burtus", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Iekļaut mazos burtus", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Iekļaut ciparus", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Iekļaut īpašās rakstzīmes", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Vārdu skaits" @@ -376,7 +455,12 @@ "message": "Mazākais pieļaujamais īpašo rakstzīmju skaits" }, "avoidAmbChar": { - "message": "Izvairīties no viegli sajaucamām rakstzīmēm" + "message": "Izvairīties no viegli sajaucamām rakstzīmēm", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Izvairīties no viegli sajaucamām rakstzīmēm", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Meklēt glabātavā" @@ -556,6 +640,18 @@ "security": { "message": "Drošība" }, + "confirmMasterPassword": { + "message": "Apstiprināt galveno paroli" + }, + "masterPassword": { + "message": "Galvenā parole" + }, + "masterPassImportant": { + "message": "Galveno paroli nevar atgūt, ja tā tiek aizmirsta." + }, + "masterPassHintLabel": { + "message": "Galvenās paroles norāde" + }, "errorOccurred": { "message": "Atgadījās kļūda" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Jaunais konts ir izveidots. Tagad vari pieteikties." }, + "newAccountCreated2": { + "message": "Jaunais konts tika izveidots." + }, + "youHaveBeenLoggedIn": { + "message": "Tu esi pieteicies." + }, "youSuccessfullyLoggedIn": { "message": "Pieteikšanās bija veiksmīga" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Apstiprinājuma kods ir nepieciešams." }, + "webauthnCancelOrTimeout": { + "message": "Autentifikācija tika atcelta vai tā aizņēma pārāk daudz laika. Lūgums mēģināt vēlreiz." + }, "invalidVerificationCode": { "message": "Nederīgs apstiprinājuma kods" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Nolasīt autentificētāja kvadrātkodu pašreizējā tīmekļa lapā" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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": "Uzzināt vairāk par autentificētājiem" + }, "copyTOTP": { "message": "Ievietot starpliktuvē autentificētāja atslēgu (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Pieteikšanās sesija ir beigusies." }, + "logIn": { + "message": "Pieteikties" + }, + "restartRegistration": { + "message": "Sākt reģistrēšanos no jauna" + }, + "expiredLink": { + "message": "Saitei beidzies derīgums" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Lūgums sākt reģistrēšanos no jauna vai mēģināt pieteikties." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Tev jau varētu būt konts" + }, "logOutConfirmation": { "message": "Vai tiešām atteikties?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Jauns URI" }, + "addDomain": { + "message": "Pievienot domēna vārdu", + "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": { "message": "Vienums pievienots" }, @@ -737,11 +873,17 @@ "enableAddLoginNotification": { "message": "Vaicāt, lai pievienotu pieteikšanās vienumu" }, + "vaultSaveOptionsTitle": { + "message": "Saglabāt glabātavas iespējās" + }, "addLoginNotificationDesc": { "message": "Vaicāt pievienot vienumu, ja tāds nav atrodams glabātavā." }, "addLoginNotificationDescAlt": { - "message": "Vaicāt, vai pievienot vienumu, ja glabātavā tāds nav atrodams. Attiecas uz visiem pieslēgtajiem kontiem." + "message": "Vaicāt, vai pievienot vienumu, ja glabātavā tāds nav atrodams. Attiecas uz visiem kontiem, kuri ir pieteikušies." + }, + "showCardsInVaultView": { + "message": "Rādīt kartes kā automātiskās aizpildes ieteikumus glabātavas skatā" }, "showCardsCurrentTab": { "message": "Rādīt kartes cilnes lapā" @@ -749,6 +891,9 @@ "showCardsCurrentTabDesc": { "message": "Attēlot kartes ciļņu lapā vieglākai aizpildīšanai." }, + "showIdentitiesInVaultView": { + "message": "Rādīt identitātes kā automātiskās aizpildes ieteikumus glabātavas skatā" + }, "showIdentitiesCurrentTab": { "message": "Rādīt identitātes cilnes pārskatā" }, @@ -776,13 +921,13 @@ "message": "Vaicāt atjaunināt pieteikšanās vienuma paroli, ja tīmekļvietnē ir noteiktas tās izmaiņas." }, "changedPasswordNotificationDescAlt": { - "message": "Vaicāt, vai atjaunināt pieteikšanās vienuma paroli, kad tīmekļvietnē ir noteikta atšķirība. Attiecas uzvisiem pieslēgtajiem kontiem." + "message": "Vaicāt, vai atjaunināt pieteikšanās vienuma paroli, kad tīmekļvietnē ir noteikta atšķirība. Attiecas uz visiem kontiem, kuri ir pieteikušies." }, "enableUsePasskeys": { "message": "Vaicāt, vai saglabāt un izmantot piekļuves atslēgas" }, "usePasskeysDesc": { - "message": "Vaicāt, vai saglabāt jaunas piekļuves atslēgas vai pieteikties ar glabātavā esošajām piekļuves atslēgām. Attiecas uz visiem pieslēgtajiem kontiem." + "message": "Vaicāt, vai saglabāt jaunas piekļuves atslēgas vai pieteikties ar glabātavā esošajām piekļuves atslēgām. Attiecas uz visiem kontiem, kuri ir pieteikušies." }, "notificationChangeDesc": { "message": "Vai atjaunināt šo paroli Bitwarden?" @@ -806,11 +951,11 @@ "message": "Izmantot otrējo klikšķi, lai piekļūtu paroļu veidošanai un tīmekļvietnei atbilstošajiem pieteikšanās vienumiem." }, "contextMenuItemDescAlt": { - "message": "Izmantot otrējo klikšķi, lai piekļūtu paroļu veidošanai un tīmekļvietnei atbilstošajiem pieteikšanās vienumiem. Attiecas uz visiem pieslēgtajiem kontiem." + "message": "Izmantot otrējo klikšķi, lai piekļūtu paroļu veidošanai un tīmekļvietnei atbilstošajiem pieteikšanās vienumiem. Attiecas uz visiem kontiem, kuri ir pieteikušies." }, "defaultUriMatchDetection": { "message": "Noklusējuma URI atbilstības noteikšana", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Izvēlēties noklusējuma veidu, kādā tiek apstrādāta pieteikšanās vienumu URI atbilstības noteikšana, kad tiek veiktas tādas darbības kā automātiska aizpilde." @@ -822,7 +967,7 @@ "message": "Mainīt lietotnes izskata krāsas." }, "themeDescAlt": { - "message": "Mainīt lietotnes izskata krāsas. Attiecas uz visiem pieslēgtajiem kontiem." + "message": "Mainīt lietotnes izskata krāsas. Attiecas uz visiem kontiem, kuri ir pieteikušies." }, "dark": { "message": "Tumšs", @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB šifrētas krātuves datņu pielikumiem." }, + "premiumSignUpEmergency": { + "message": "Ārkārtas piekļuve" + }, "premiumSignUpTwoStepOptions": { "message": "Tādas slēgtā pirmavota divpakāpju pieteikšanās iespējas kā YubiKey un Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Premium dalību ir iespējams iegādāties bitwarden.com tīmekļa glabātavā. Vai tagad apmeklēt tīmekļvietni?" }, + "premiumPurchaseAlertV2": { + "message": "Premium var iegādāties Bitwarden tīmekļa lietotnē sava konta iestatījumos." + }, "premiumCurrentMember": { "message": "Tu esi Premium dalībnieks!" }, "premiumCurrentMemberThanks": { "message": "Paldies, ka atbalsti Bitwarden!" }, + "premiumFeatures": { + "message": "Uzlabo uz \"Premium\" un saņem:" + }, "premiumPrice": { "message": "Viss par tikai $PRICE$ gadā!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "Viss par tikai $PRICE$ gadā.", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Atsvaidzināšana pabeigta" }, @@ -1179,10 +1342,19 @@ }, "showAutoFillMenuOnFormFields": { "message": "Rādīt automātiskās aizpildes izvēlni veidlapu laukos", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { - "message": "Attiecas uz visiem pieslēgtajiem kontiem." + "autofillSuggestionsSectionTitle": { + "message": "Automātiskās aizpildes ieteikumi" + }, + "showInlineMenuLabel": { + "message": "Rādīt automātiskās aizpildes ieteikumuis veidlapu laukos" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Attēlot ieteikumus, kad tiek atlasīta ikona" + }, + "showInlineMenuOnFormFieldsDescAlt": { + "message": "Attiecas uz visiem kontiem, kuri ir pieteikušies." }, "turnOffBrowserBuiltInPasswordManagerSettings": { "message": "Jāizslēdz sava pārlūka iebūvētā paroļu pārvaldnieka iestatījumi, lai izvairītos no nesaderībām." @@ -1202,15 +1374,34 @@ "message": "Kad tiek atlasīta automātiskās aizpildes ikona", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Automātiski aizpildīt lapas ielādes brīdī" + }, "enableAutoFillOnPageLoad": { "message": "Automātiski aizpildīt lapas ielādes brīdī" }, "enableAutoFillOnPageLoadDesc": { "message": "Ja tiek noteikta pieteikšanās veidne, tā tiks aizpildīta lapas ielādes brīdī." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Brīdinājums:$CLOSETAG$ pārveidotās vai neuzticamās tīmekļvietnēs automātiskā aizpilde lapas ielādes laikā var tikt ļaunprātīgi izmantota.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Pārveidotās vai neuzticamās tīmekļvietnēs automātiskā aizpilde lapas ielādes laikā var tikt ļaunprātīgi izmantota." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Uzzināt vairāk par iespējamām bīstamībām" + }, "learnMoreAboutAutofill": { "message": "Uzzināt vairāk par automātisko aizpildi" }, @@ -1238,9 +1429,15 @@ "commandOpenSidebar": { "message": "Atvērt glabātavu sānu joslā" }, - "commandAutofillDesc": { + "commandAutofillLoginDesc": { "message": "Automātiski aizpildīt ar iepriekš izmantoto pieteikšanās vienumu pašreizējā tīmekļvietnē" }, + "commandAutofillCardDesc": { + "message": "Automātiski aizpildīt ar iepriekš izmantoto karti pašreizējā tīmekļvietnē" + }, + "commandAutofillIdentityDesc": { + "message": "Automātiski aizpildīt ar iepriekš izmantoto identitāti pašreizējā tīmekļvietnē" + }, "commandGeneratePasswordDesc": { "message": "Izveidot jaunu nejaušu paroli un ievietot to starpliktuvē" }, @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Patiesuma vērtība" }, + "cfTypeCheckbox": { + "message": "Izvēles rūtiņa" + }, "cfTypeLinked": { "message": "Saistīts", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "Apskatīt $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Paroļu vēsture" }, @@ -1533,6 +1742,10 @@ "message": "Pamata domēns", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Pamata domēns (ieteicams)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domēna vārds", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Atbilstības noteikšana", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Noklusētā atbilstības noteikšana", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Pārslēgt iespējas" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Nav paroļu, ko parādīt." }, + "clearHistory": { + "message": "Notīrīt vēsturi" + }, + "noPasswordsToShow": { + "message": "Nav paroļu, ko parādīt" + }, + "noRecentlyGeneratedPassword": { + "message": "Pēdējā laikā nav izveidota neviena parole" + }, "remove": { "message": "Noņemt" }, @@ -1677,6 +1899,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" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Jaunā galvenā parole neatbilst nosacījumu prasībām." }, - "receiveMarketingEmails": { - "message": "Saņemt e-pasta ziņojumus no Bitwarden par paziņojumiem, padomiem un izpētes iespējām." + "receiveMarketingEmailsV2": { + "message": "Iegūt savā iesūtnē padomus, paziņojumus un izpētes iespējas no Bitwarden." }, "unsubscribe": { "message": "Atteikt abonēšanu" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Lūgums atslēgt šo lietotāju darbvirsmas lietotnē un mēģināt vēlreiz." }, + "biometricsNotAvailableTitle": { + "message": "Atslēgšana ar biometriju nav pieejama" + }, + "biometricsNotAvailableDesc": { + "message": "Atslēgšana ar biometriju pašlaik nav pieejama. Lūgums vēlāk mēģināt vēlreiz." + }, "biometricsFailedTitle": { "message": "Biometrija neizdevās" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "Apvienības nosacījums neļauj ievietot ārējos vienumus savā personīgajā glabātavā." }, + "domainsTitle": { + "message": "Domēna vārdi", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Izņēmuma domēni" }, @@ -1926,17 +2187,29 @@ "message": "Bitwarden nevaicās saglabāt pieteikšanās datus šiem domēniem. Ir jāpārlādē lapa, lai izmaiņas iedarbotos." }, "excludedDomainsDescAlt": { - "message": "Bitwarden nevaicās saglabāt pieteikšanās datus šiem domēniem visiem pieslēgtajiem kontiem. Ir jāpārlādē lapa, lai iedarbotos izmaiņas." + "message": "Bitwarden nevaicās saglabāt pieteikšanās datus visiem šī domēna kontiem, kuri ir pieteikušies. Ir jāpārlādē lapa, lai iedarbotos izmaiņas." + }, + "websiteItemLabel": { + "message": "Tīmekļvietne $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ nav derīgs domēns", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Saglabātas vērā neņemto domēna vārdu izmaiņas" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Aizsargāts ar paroli" }, + "copyLink": { + "message": "Ievietot saiti starpliktuvē" + }, "copySendLink": { "message": "Ievietot Send saiti starpliktuvē", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send izveidots", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send tika veiksmīgi izveidots.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "Send būs pieejams nākamās $DAYS$ dienas ikvienam, kuram ir saite.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send saite ievietota starpliktuvē", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saglabāts", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Nepieciešama e-pasta adreses apstiprināšana" }, + "emailVerifiedV2": { + "message": "E-pasta adrese ir apliecināta" + }, "emailVerificationRequiredDesc": { "message": "Ir nepieciešams apstiprināt e-pasta adresi, lai būtu iespējams izmantot šo iespēju. To var izdarīt tīmekļa glabātavā." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automātiska ievietošana sarakstā" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Automātiskās aizpildes iestatījumi" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Automātiskās aizpildes īsinājumtaustiņi" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Mainīt īsinājumtaustiņus" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Pārvaldīt saīsnes" + }, "autofillShortcut": { "message": "Automātiskās aizpildes īsinājumtaustiņi" }, - "autofillShortcutNotSet": { - "message": "Automātiskās aizpildes īsceļš nav uzstādīts. To var izdarīt pārlūka iestatījumos." + "autofillLoginShortcutNotSet": { + "message": "Automātiskās aizpildes īsceļš pieteikšanās vienumiem nav uzstādīts. To var izdarīt pārlūka iestatījumos." }, - "autofillShortcutText": { - "message": "Automātiskās aizpildes īsceļš ir: $COMMAND$. To var mainīt pārlūka iestatījumos.", + "autofillLoginShortcutText": { + "message": "Automātiskās aizpildes īsceļš pieteikšanās vienumiem ir: $COMMAND$. Visus īsinājumtaustiņus var pārvaldīt pārlūka iestatījumos.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Ierīce ir uzticama" }, + "sendsNoItemsTitle": { + "message": "Nav spēkā esošu Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Send ir izmantojams, lai ar ikvienu droši kopīgotu šifrētu informāciju.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Jāievada vērtība." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 laukam jāpievērš uzmanība." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ laukiem ir jāpievērš uzmanība.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Atlasīt --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Vienumus ar galvenās paroles pārvaicāšanu nevar automātiski aizpildīt lapas ielādes brīdī. Automātiskā aizpilde lapas ielādes brīdī ir izslēgta.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "Automātiskā aizpilde lapas ielādes brīdī iestatīta izmantot noklusējuma iestatījumu.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Jāizslēdz galvenās paroles pārvaicāšana, lai labotu šo lauku", @@ -2911,10 +3240,18 @@ "message": "Jāatslēdz savs konts, lai apskatītu atbilstošus pieteikšanās vienumus", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Jāatslēdz savs konts, lai apskatītu automātiskās aizpildes ieteikumus", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Atslēgt kontu", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Atslēgt savu kontu, tiks atvērts jaunā logā", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Aizpildīt pieteikšanās datus", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Pievienot jaunu glabātavas vienumu", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "Jauns pieteikšanās vienums", + "description": "Button text to display within inline menu when there are no matching items on a login field" + }, + "addNewLoginItemAria": { + "message": "Pievienot jaunu glabātavas pieteikšanās vienumu, tiks atvērts jaunā logā", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "Jauna karte", + "description": "Button text to display within inline menu when there are no matching items on a credit card field" + }, + "addNewCardItemAria": { + "message": "Pievienot jaunu glabātavas kartes vienumu, tiks atvērts jaunā logā", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "Jauna identitāte", + "description": "Button text to display within inline menu when there are no matching items on an identity field" + }, + "addNewIdentityItemAria": { + "message": "Pievienot jaunu glabātavas identitātes vienumu, tiks atvērts jaunā logā", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Ir pieejama Bitwarden automātiskās aizpildes izvēlne. Jānospiež poga ar bultu uz leju, lai atlasītu.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Kļūda savienojuma izveidošanā ar Duo pakalpojumu. Jāizmanto cits divpakāpju pieteikšanāš veids vai jāvēršas pie Duo pēc palīdzības." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Jāpalaiž DUO un jāseko soļiem, lai pabeigtu pieteikšanos." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Nederīga datnes parole, lūgums izmantot to paroli, kas tika ievadīta izgūšanas datnes izveidošanas brīdī." }, - "importDestination": { - "message": "Ievietošanas galamērķis" + "destination": { + "message": "Galamērķis" }, "learnAboutImportOptions": { "message": "Uzzināt par ievietošanas iespējām" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Vietne, kurā tika uzsākta darbība, pieprasa pārbaudi. Šī iespēja vēl nav īstenota kontiem, kuriem nav galvenās paroles." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Pieteikties ar piekļuves atslēgu?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "Nav šai vietnei atbilstoša pieteikšanās vienuma." }, + "noMatchingLoginsForSite": { + "message": "Šai vietnei nav atbilstošu pieteikšanās vietnumu" + }, "confirm": { "message": "Apstiprināt" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Saglabāt piekļuves atslēgu kā jaunu pieteikšanās vienumu" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Izvēlēties pieteikšanās vienumu, kurā saglabāt šo piekļuves atslēgu" }, + "chooseCipherForPasskeyAuth": { + "message": "Izvēlēties piekļuves atslēgu, ar kuru pieteikties" + }, "passkeyItem": { "message": "Piekļuves atslēgas vienums" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,6 +3659,30 @@ "message": "Izplatīti veidoli", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Pāriet uz pārlūka iestatījumiem?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Pāriet uz palīdzības centru?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" + }, + "confirmContinueToHelpCenterPasswordManagementContent": { + "message": "Jāmaina sava pārlūka automātiskās aizpildes un paroļu pārvaldības iestatījumi.", + "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": "Paplašinājuma īsinājumtaustiņus skatīt un iestatīt var pārlūka iestatījumos.", + "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": "Jāmaina sava pārlūka automātiskās aizpildes un paroļu pārvaldības iestatījumi.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" + }, + "confirmContinueToBrowserKeyboardShortcutSettingsContent": { + "message": "Paplašinājuma īsinājumtaustiņus skatīt un iestatīt var pārlūka iestatījumos.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Padarīt Bitwarden par noklusējuma paroļu pārvaldnieku?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" @@ -3317,10 +3711,18 @@ "message": "Piekļuves informācija veiksmīgi saglabāta.", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Parole saglabāta.", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Piekļuves informācija veiksmīgi atjaunināta.", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Parole atjaunināta.", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Kļūda piekļuves informācijas saglabāšanā. Jāpārbauda, vai konsolē ir izvērstāka informācija.", "description": "Notification message for when saving credentials has failed." @@ -3334,20 +3736,6 @@ "passkeyRemoved": { "message": "Piekļuves atslēga noņemta" }, - "unassignedItemsBannerNotice": { - "message": "Jāņem vērā: nepiešķirti apvienības vienumi vairs nav redzami skatā \"Visas glabātavas\" un ir pieejami tikai pārvaldības konsolē." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Jāņem vērā: no 2024. gada 16. maija nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" un būs pieejami tikai pārvaldības konsolē." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Piešķirt šos vienumus krājumam", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "lai padarītu tos redzamus.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Ieteikumi automātiskajai aizpildei" }, @@ -3415,7 +3803,7 @@ }, "autofillTitle": { "message": "Automātiski aizpildīt - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "Nav vērtību, ko ievietot starpliktuvē" }, - "assignCollections": { - "message": "Piešķirt krājumus" + "assignToCollections": { + "message": "Piešķirt krājumiem" }, "copyEmail": { "message": "Ievietot starpliktuvē e-pasta adresi" @@ -3493,13 +3881,13 @@ "message": "Vienumi bez mapes" }, "itemDetails": { - "message": "Item details" + "message": "Vienuma dati" }, "itemName": { - "message": "Item name" + "message": "Vienuma nosaukums" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Nevar noņemt krājumus ar tiesībām \"Tikai skatīt\": $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3899,33 @@ "message": "Apvienība ir atspējota" }, "owner": { - "message": "Owner" + "message": "Īpašnieks" }, "selfOwnershipLabel": { - "message": "You", + "message": "Tu", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Atspējotu apvienību vienumiem nevar piekļūt. Jāsazinās ar apvienības īpašnieku, lai iegūtu palīdzību." }, + "additionalInformation": { + "message": "Papildu informācija" + }, + "itemHistory": { + "message": "Vienuma vēsture" + }, + "lastEdited": { + "message": "Pēdējo reizi labots" + }, + "ownerYou": { + "message": "Īpašnieks: Tu" + }, + "linked": { + "message": "Saistīts" + }, + "copySuccessful": { + "message": "Ievietošana starpliktuvē veiksmīga" + }, "upload": { "message": "Augšupielādēt" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Atlases" + }, + "personalDetails": { + "message": "Personiskā informācija" + }, + "identification": { + "message": "Identifikācija" + }, + "contactInfo": { + "message": "Saziņas informācija" + }, + "downloadAttachment": { + "message": "Lejupielādēt $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "kartes numurs beidzas ar", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "message": "Automātiskās aizpildes iespējas" + }, + "websiteUri": { + "message": "Tīmekļvietne (URI)" + }, + "websiteUriCount": { + "message": "Tīmekļvietne (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": "Tīmekļvietne pievienota" + }, + "addWebsite": { + "message": "Pievient tīmekļvietni" + }, + "deleteWebsite": { + "message": "Izdzēst tīmekļvietni" + }, + "defaultLabel": { + "message": "Noklusējums ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Rādīt atbilstības noteikšanu $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Paslēpt atbilstības noteikšanu $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Automātiski aizpildīt lapas ielādes brīdī?" + }, + "cardExpiredTitle": { + "message": "Beidzies kartes derīgums" + }, + "cardExpiredMessage": { + "message": "Ja atjaunoji to, jāatjaunina kartes informācija" + }, + "cardDetails": { + "message": "Kartes dati" + }, + "cardBrandDetails": { + "message": "$BRAND$ dati", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Iespējot animācijas" + }, + "addAccount": { + "message": "Pievienot kontu" + }, + "loading": { + "message": "Ielādē" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Vienumu varēs redzēt tikai apvnienības dalībnieki ar piekļuvi šiem krājumiem." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Vienumus varēs redzēt tikai apvnienības dalībnieki ar piekļuvi šiem krājumiem." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Pievienot lauku" + }, + "add": { + "message": "Pievienot" + }, + "fieldType": { + "message": "Lauka veids" + }, + "fieldLabel": { + "message": "Lauka iezīme" + }, + "textHelpText": { + "message": "Teksta lauki ir izmantojami tādai informācijai kā drošības jautājumi" + }, + "hiddenHelpText": { + "message": "Paslēptie lauki ir izmantojami tādai slepenai informācijai kā parole" + }, + "checkBoxHelpText": { + "message": "Izvēles rūtiņas ir izmantojamas, ja ir vajadzība automātiski aizpildīt veidlapas izvēles rūtiņu, piemēram, atcerēties e-pasta adresi" + }, + "linkedHelpText": { + "message": "Saistītais lauks ir izmantojams, kad noteiktā lapā tiek pieredzētas nepilnības ar automātisko aizpildi." + }, + "linkedLabelHelpText": { + "message": "Jāievada lauka HTML id, name, aria-label vai placeholder vērtība." + }, + "editField": { + "message": "Labot lauku" + }, + "editFieldLabel": { + "message": "Labot $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Izdzēst $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ pievienots", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Pārkārtot $LABEL$. Jāizmanto bultas taustiņš, lai pārvietotu vienumu augšup vai lejup.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ pārvietots augšup, $INDEX$. no $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 vienums tiks neatgriezeniski nodots atlasītajai apvienībai. Šis vienums Tev vairs nepiederēs." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ vienumi tiks neatgriezeniski nodoti atlasītajai apvienībai. Šie vienumi Tev vairs nepiederēs.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 vienums tiks neatgriezeniski nodots $ORG$. Šis vienums Tev vairs nepiederēs.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ vienumi tiks neatgriezeniski nodoti $ORG$. Šie vienumi Tev vairs nepiederēs.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Vienumi pārvietoti uz $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Vienums pārvietots uz $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ pārvietots lejup, $INDEX$. no $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Vienuma atrašanās vieta" + }, + "fileSends": { + "message": "Datņu Send" + }, + "textSends": { + "message": "Teksta Send" + }, + "bitwardenNewLook": { + "message": "Bitwarden ir jauns izskats." + }, + "bitwardenNewLookDesc": { + "message": "Veikt automātisko aizpildi un meklēšanu glabātavas cilnē ir vienkāršāk un izprotamāk kā jebkad. Apskati izmaiņas!" + }, + "accountActions": { + "message": "Konta darbības" + }, + "showNumberOfAutofillSuggestions": { + "message": "Paplašinājuma ikonā rādīt pieteikšanās automātiskās aizpildes ieteikumu skaitu" + }, + "systemDefault": { + "message": "Sistēmas noklusējums" + }, + "enterprisePolicyRequirementsApplied": { + "message": "Šim iestatījumam tika piemērotas uzņēmējdarbības nosacījumu prasības" + }, + "fileSavedToDevice": { + "message": "Datne saglabāta ierīcē. Tā ir atrodama ierīces lejupielāžu mapē." + }, + "showCharacterCount": { + "message": "Rādīt rakstzīmju skaitu" + }, + "hideCharacterCount": { + "message": "Paslēpt rakstzīmju skaitu" + }, + "itemsInTrash": { + "message": "Vienumi atkritnē" + }, + "noItemsInTrash": { + "message": "Atkritnē nav vienumu" + }, + "noItemsInTrashDesc": { + "message": "Izdzēstie vienumi parādīsies šeit, un tie tiks neatgriezeniski izdzēsti pēc 30 dienām" + }, + "trashWarning": { + "message": "Vienumi, kas atkritnē atrodas vairāk nekā 30 dienas, tiks automatiski izdzēsti" + }, + "restore": { + "message": "Atjaunot" + }, + "deleteForever": { + "message": "Izdzēst pavisam" + }, + "noEditPermissions": { + "message": "Nav nepieciešamo atļauju, lai labotu šo vienumu" } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index eb94b7e992a..bcec218774d 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "നിങ്ങളുടെ സുരക്ഷിത വാൾട്ടിലേക്കു പ്രവേശിക്കാൻ ലോഗിൻ ചെയ്യുക അല്ലെങ്കിൽ ഒരു പുതിയ അക്കൗണ്ട് സൃഷ്ടിക്കുക." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "അക്കൗണ്ട് സൃഷ്ടിക്കുക" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "ലോഗിൻ" - }, "enterpriseSingleSignOn": { "message": "എന്റർപ്രൈസ് സിംഗിൾ സൈൻ-ഓൺ" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "പ്രാഥമിക പാസ്‌വേഡ് സൂചന (ഇഷ്ടാനുസൃതമായ)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "ടാബ് " }, @@ -107,17 +113,32 @@ "copySecurityCode": { "message": "സുരക്ഷാ കോഡ് പകർത്തുക" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "ഓട്ടോഫിൽ" }, "autoFillLogin": { - "message": "Auto-fill login" + "message": "Autofill login" }, "autoFillCard": { - "message": "Auto-fill card" + "message": "Autofill card" }, "autoFillIdentity": { - "message": "Auto-fill identity" + "message": "Autofill identity" }, "generatePasswordCopied": { "message": "പാസ്‌വേഡ് സൃഷ്ടിക്കുക (പകർത്തുക )" @@ -280,6 +301,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,16 +384,56 @@ "message": "Minimum password length" }, "uppercase": { - "message": "Uppercase (A-Z)" + "message": "Uppercase (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Lowercase (a-z)" + "message": "Lowercase (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numbers (0-9)" + "message": "Numbers (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Special characters (!@#$%^&*)" + "message": "Special characters (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "വാക്കുകളുടെ എണ്ണം" @@ -376,7 +455,12 @@ "message": "കുറഞ്ഞ പ്രത്യേക പ്രതീകങ്ങൾ" }, "avoidAmbChar": { - "message": "അവ്യക്തമായ പ്രതീകങ്ങൾ ഒഴിവാക്കുക" + "message": "അവ്യക്തമായ പ്രതീകങ്ങൾ ഒഴിവാക്കുക", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "വാൾട് തിരയുക" @@ -556,6 +640,18 @@ "security": { "message": "സുരക്ഷ" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "ഒരു പിഴവ് സംഭവിച്ചിരിക്കുന്നു" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "തങ്ങളുടെ അക്കൗണ്ട് സൃഷ്ടിക്കപ്പെട്ടു! ഇനി താങ്കൾക്ക് ലോഗിൻ ചെയ്യാം." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "പരിശോധിച്ചുറപ്പിക്കൽ കോഡ് ആവശ്യമാണ്." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "നിങ്ങളുടെ പ്രവർത്തന സമയം കഴിഞ്ഞിരിക്കുന്നു." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "നിങ്ങൾക്ക് ലോഗ് ഔട്ട് ചെയ്യണമെന്ന് ഉറപ്പാണോ?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "പുതിയ URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "ചേർക്കപ്പെട്ട ഇനം" }, @@ -737,23 +873,32 @@ "enableAddLoginNotification": { "message": "Ask to add login" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "നിങ്ങൾ ആദ്യമായി സൈറ്റിൽ പ്രവേശിക്കുമ്പോൾ നിങ്ങളുടെ വാൾട്ടിലേക്കു തനിയെ പ്രവേശനം ഉൾപെടുത്താൻ \"പ്രവേശനം ചേർക്കുക എന്ന അറിയിപ്പ്\" ആവശ്യപ്പെടും." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Show cards on Tab page" }, "showCardsCurrentTabDesc": { - "message": "List card items on the Tab page for easy auto-fill." + "message": "List card items on the Tab page for easy autofill." + }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" }, "showIdentitiesCurrentTabDesc": { - "message": "List identity items on the Tab page for easy auto-fill." + "message": "List identity items on the Tab page for easy autofill." }, "clearClipboard": { "message": "ക്ലിപ്ബോര്‍ഡ് മായ്ക്കുക", @@ -791,7 +936,7 @@ "message": "ശരി, ഇപ്പോൾ അപ്ഡേറ്റ് ചെയ്യുക" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Unlock" @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "സാധാരണ URI പൊരുത്തം കണ്ടെത്തൽ", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "യാന്ത്രിക പൂരിപ്പിക്കൽ പോലുള്ള പ്രവർത്തനങ്ങൾ നടത്തുമ്പോൾ ലോഗിനുകൾക്കായി യുആർഐ മാച്ച് ഡിറ്റക്ഷൻ കൈകാര്യം ചെയ്യുന്ന സ്ഥിരസ്ഥിതി മാർഗം തിരഞ്ഞെടുക്കുക." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "ഫയൽ അറ്റാച്ചുമെന്റുകൾക്കായി 1 ജിബി എൻക്രിപ്റ്റുചെയ്‌ത സംഭരണം." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "നിങ്ങൾക്ക് bitwarden.com വെബ് വാൾട്ടിൽ പ്രീമിയം അംഗത്വം വാങ്ങാം. നിങ്ങൾക്ക് ഇപ്പോൾ വെബ്സൈറ്റ് സന്ദർശിക്കാൻ ആഗ്രഹമുണ്ടോ?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "തങ്ങൾ ഒരു പ്രീമിയം അംഗമാണ്!" }, "premiumCurrentMemberThanks": { "message": "Bitwardenനെ പിന്തുണച്ചതിന് നന്ദി." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "എല്ലാം വെറും $PRICE$/ വർഷത്തേക്ക്!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "റിഫ്രഷ് പൂർത്തിയായി" }, @@ -1178,14 +1341,23 @@ "message": "എന്വിയാണമെന്റ് URL സംരക്ഷിച്ചു." }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,38 +1371,57 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "പേജ് ലോഡിൽ യാന്ത്രിക-പൂരിപ്പിക്കൽ പ്രവർത്തനക്ഷമമാക്കുക" }, "enableAutoFillOnPageLoadDesc": { "message": "ഒരു ലോഗിൻ ഫോം കണ്ടെത്തിയാൽ, വെബ് പേജ് ലോഡുചെയ്യുമ്പോൾ യാന്ത്രികമായി ഒരു സ്വയം പൂരിപ്പിക്കൽ നടത്തുക." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Compromised or untrusted websites can exploit autofill on page load." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" }, "learnMoreAboutAutofill": { - "message": "Learn more about auto-fill" + "message": "Learn more about autofill" }, "defaultAutoFillOnPageLoad": { "message": "Default autofill setting for login items" }, "defaultAutoFillOnPageLoadDesc": { - "message": "You can turn off auto-fill on page load for individual login items from the item's Edit view." + "message": "You can turn off autofill on page load for individual login items from the item's Edit view." }, "itemAutoFillOnPageLoad": { - "message": "Auto-fill on page load (if set up in Options)" + "message": "Autofill on page load (if set up in Options)" }, "autoFillOnPageLoadUseDefault": { "message": "Use default setting" }, "autoFillOnPageLoadYes": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "autoFillOnPageLoadNo": { - "message": "Do not auto-fill on page load" + "message": "Do not autofill on page load" }, "commandOpenPopup": { "message": "വോൾട്ട് പോപ്പ്അപ്പ് തുറക്കുക" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "വാൾട് സൈഡ്ബാറിൽ തുറക്കുക" }, - "commandAutofillDesc": { - "message": "നിലവിലെ വെബ്‌സൈറ്റിനായി അവസാനമായി ഉപയോഗിച്ച ലോഗിൻ യാന്ത്രികമായി പൂരിപ്പിക്കുക" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "ക്ലിപ്പ്ബോർഡിലേക്ക് ഒരു പുതിയ റാൻഡം പാസ്‌വേഡ് സൃഷ്ടിച്ച് പകർത്തുക" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "ബൂളിയൻ" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Linked", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "പാസ്സ്‌വേഡ് നാൾവഴി" }, @@ -1533,6 +1742,10 @@ "message": "അടിസ്ഥാന ഡൊമെയ്ൻ", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domain name", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "പൊരുത്തം കണ്ടെത്തൽ", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "സ്ഥിരസ്ഥിതി പൊരുത്ത കണ്ടെത്തൽ", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "ഓപ്ഷനുകൾ ടോഗിൾ ചെയ്യുക" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "പ്രദർശിപ്പിക്കാൻ പാസ്സ്‌വേഡുകൾ ഒന്നും ഇല്ല." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "നീക്കുക" }, @@ -1677,6 +1899,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": "വാൾട് ടൈം ഔട്ട് ആക്ഷൻ" }, @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "നിങ്ങളുടെ പുതിയ മാസ്റ്റർ പാസ്‌വേഡ് നയ ആവശ്യകതകൾ നിറവേറ്റുന്നില്ല." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ is not a valid domain", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Password protected" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Copy Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send created", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2606,10 +2906,10 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { - "message": "How to auto-fill" + "message": "How to autofill" }, "autofillSelectInfoWithCommand": { "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", @@ -2627,16 +2927,25 @@ "message": "Got it" }, "autofillSettings": { - "message": "Auto-fill settings" + "message": "Autofill settings" + }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index e73ccc0b13a..0ecb9907504 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "तुमच्या सुरक्षित तिजोरीत पोहचण्यासाठी लॉग इन करा किंवा नवीन खाते उघडा." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "खाते तयार करा" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "प्रवेश करा" - }, "enterpriseSingleSignOn": { "message": "Enterprise single sign-on" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "मुख्य पासवर्डचा संकेत (पर्यायी)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "टॅब" }, @@ -107,17 +113,32 @@ "copySecurityCode": { "message": "सुरक्षा कोड कॉपी करा" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "स्वयंभरण" }, "autoFillLogin": { - "message": "Auto-fill login" + "message": "Autofill login" }, "autoFillCard": { - "message": "Auto-fill card" + "message": "Autofill card" }, "autoFillIdentity": { - "message": "Auto-fill identity" + "message": "Autofill identity" }, "generatePasswordCopied": { "message": "Generate password (copied)" @@ -150,7 +171,7 @@ "message": "तिजोरीत प्रवेश करा" }, "autoFillInfo": { - "message": "There are no logins available to auto-fill for the current browser tab." + "message": "There are no logins available to autofill for the current browser tab." }, "addLogin": { "message": "लॉगिन जोडा" @@ -280,6 +301,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,16 +384,56 @@ "message": "Minimum password length" }, "uppercase": { - "message": "Uppercase (A-Z)" + "message": "Uppercase (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Lowercase (a-z)" + "message": "Lowercase (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numbers (0-9)" + "message": "Numbers (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Special characters (!@#$%^&*)" + "message": "Special characters (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Number of words" @@ -376,7 +455,12 @@ "message": "Minimum special" }, "avoidAmbChar": { - "message": "Avoid ambiguous characters" + "message": "Avoid ambiguous characters", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "तिजोरीत शोधा" @@ -556,6 +640,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -613,7 +718,7 @@ } }, "autofillError": { - "message": "Unable to auto-fill the selected item on this page. Copy and paste the information instead." + "message": "Unable to autofill the selected item on this page. Copy and paste the information instead." }, "totpCaptureError": { "message": "Unable to scan QR code from the current webpage" @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Your login session has expired." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "New URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "Item added" }, @@ -737,23 +873,32 @@ "enableAddLoginNotification": { "message": "Ask to add login" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "Ask to add an item if one isn't found in your vault." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Show cards on Tab page" }, "showCardsCurrentTabDesc": { - "message": "List card items on the Tab page for easy auto-fill." + "message": "List card items on the Tab page for easy autofill." + }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" }, "showIdentitiesCurrentTabDesc": { - "message": "List identity items on the Tab page for easy auto-fill." + "message": "List identity items on the Tab page for easy autofill." }, "clearClipboard": { "message": "Clear clipboard", @@ -791,7 +936,7 @@ "message": "Update" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Unlock" @@ -810,10 +955,10 @@ }, "defaultUriMatchDetection": { "message": "Default URI match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { - "message": "Choose the default way that URI match detection is handled for logins when performing actions such as auto-fill." + "message": "Choose the default way that URI match detection is handled for logins when performing actions such as autofill." }, "theme": { "message": "Theme" @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a Premium member!" }, "premiumCurrentMemberThanks": { "message": "Thank you for supporting Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "All for just $PRICE$ /year!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Refresh complete" }, @@ -1028,7 +1191,7 @@ "message": "Copy TOTP automatically" }, "disableAutoTotpCopyDesc": { - "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login." + "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you autofill the login." }, "enableAutoBiometricsPrompt": { "message": "Ask for biometrics on launch" @@ -1178,14 +1341,23 @@ "message": "Environment URLs saved" }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,38 +1371,57 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "enableAutoFillOnPageLoadDesc": { - "message": "If a login form is detected, auto-fill when the web page loads." + "message": "If a login form is detected, autofill when the web page loads." + }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Compromised or untrusted websites can exploit autofill on page load." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" }, "learnMoreAboutAutofill": { - "message": "Learn more about auto-fill" + "message": "Learn more about autofill" }, "defaultAutoFillOnPageLoad": { "message": "Default autofill setting for login items" }, "defaultAutoFillOnPageLoadDesc": { - "message": "You can turn off auto-fill on page load for individual login items from the item's Edit view." + "message": "You can turn off autofill on page load for individual login items from the item's Edit view." }, "itemAutoFillOnPageLoad": { - "message": "Auto-fill on page load (if set up in Options)" + "message": "Autofill on page load (if set up in Options)" }, "autoFillOnPageLoadUseDefault": { "message": "Use default setting" }, "autoFillOnPageLoadYes": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "autoFillOnPageLoadNo": { - "message": "Do not auto-fill on page load" + "message": "Do not autofill on page load" }, "commandOpenPopup": { "message": "Open vault popup" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Open vault in sidebar" }, - "commandAutofillDesc": { - "message": "Auto-fill the last used login for the current website" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Generate and copy a new random password to the clipboard" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Linked", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -1533,6 +1742,10 @@ "message": "Base domain", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domain name", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Match detection", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Default match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Toggle options" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "There are no passwords to list." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Remove" }, @@ -1677,6 +1899,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" }, @@ -1716,16 +1961,16 @@ "message": "Timeout action confirmation" }, "autoFillAndSave": { - "message": "Auto-fill and save" + "message": "Autofill and save" }, "fillAndSave": { "message": "Fill and save" }, "autoFillSuccessAndSavedUri": { - "message": "Item auto-filled and URI saved" + "message": "Item autofilled and URI saved" }, "autoFillSuccess": { - "message": "Item auto-filled " + "message": "Item autofilled " }, "insecurePageWarning": { "message": "Warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page." @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ is not a valid domain", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Password protected" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Copy Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send created", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2606,10 +2906,10 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { - "message": "How to auto-fill" + "message": "How to autofill" }, "autofillSelectInfoWithCommand": { "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", @@ -2627,16 +2927,25 @@ "message": "Got it" }, "autofillSettings": { - "message": "Auto-fill settings" + "message": "Autofill settings" + }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 95ec45da720..338d70eaf58 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "Create account" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "Log in" - }, "enterpriseSingleSignOn": { "message": "Enterprise single sign-on" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Master password hint (optional)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "Tab" }, @@ -107,17 +113,32 @@ "copySecurityCode": { "message": "Copy security code" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { - "message": "Auto-fill" + "message": "Autofill" }, "autoFillLogin": { - "message": "Auto-fill login" + "message": "Autofill login" }, "autoFillCard": { - "message": "Auto-fill card" + "message": "Autofill card" }, "autoFillIdentity": { - "message": "Auto-fill identity" + "message": "Autofill identity" }, "generatePasswordCopied": { "message": "Generate password (copied)" @@ -150,7 +171,7 @@ "message": "Log in to your vault" }, "autoFillInfo": { - "message": "There are no logins available to auto-fill for the current browser tab." + "message": "There are no logins available to autofill for the current browser tab." }, "addLogin": { "message": "Add a login" @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "Minimum password length" }, "uppercase": { - "message": "Uppercase (A-Z)" + "message": "Uppercase (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Lowercase (a-z)" + "message": "Lowercase (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numbers (0-9)" + "message": "Numbers (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Special characters (!@#$%^&*)" + "message": "Special characters (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Number of words" @@ -376,7 +455,12 @@ "message": "Minimum special" }, "avoidAmbChar": { - "message": "Avoid ambiguous characters" + "message": "Avoid ambiguous characters", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Search vault" @@ -556,6 +640,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -613,7 +718,7 @@ } }, "autofillError": { - "message": "Unable to auto-fill the selected item on this page. Copy and paste the information instead." + "message": "Unable to autofill the selected item on this page. Copy and paste the information instead." }, "totpCaptureError": { "message": "Unable to scan QR code from the current webpage" @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Your login session has expired." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "New URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "Item added" }, @@ -737,23 +873,32 @@ "enableAddLoginNotification": { "message": "Ask to add login" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "Ask to add an item if one isn't found in your vault." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Show cards on Tab page" }, "showCardsCurrentTabDesc": { - "message": "List card items on the Tab page for easy auto-fill." + "message": "List card items on the Tab page for easy autofill." + }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" }, "showIdentitiesCurrentTabDesc": { - "message": "List identity items on the Tab page for easy auto-fill." + "message": "List identity items on the Tab page for easy autofill." }, "clearClipboard": { "message": "Clear clipboard", @@ -791,7 +936,7 @@ "message": "Update" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Unlock" @@ -810,10 +955,10 @@ }, "defaultUriMatchDetection": { "message": "Default URI match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { - "message": "Choose the default way that URI match detection is handled for logins when performing actions such as auto-fill." + "message": "Choose the default way that URI match detection is handled for logins when performing actions such as autofill." }, "theme": { "message": "Theme" @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a Premium member!" }, "premiumCurrentMemberThanks": { "message": "Thank you for supporting Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "All for just $PRICE$ /year!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Refresh complete" }, @@ -1028,7 +1191,7 @@ "message": "Copy TOTP automatically" }, "disableAutoTotpCopyDesc": { - "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login." + "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you autofill the login." }, "enableAutoBiometricsPrompt": { "message": "Ask for biometrics on launch" @@ -1178,14 +1341,23 @@ "message": "Environment URLs saved" }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,38 +1371,57 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "enableAutoFillOnPageLoadDesc": { - "message": "If a login form is detected, auto-fill when the web page loads." + "message": "If a login form is detected, autofill when the web page loads." + }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Compromised or untrusted websites can exploit autofill on page load." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" }, "learnMoreAboutAutofill": { - "message": "Learn more about auto-fill" + "message": "Learn more about autofill" }, "defaultAutoFillOnPageLoad": { "message": "Default autofill setting for login items" }, "defaultAutoFillOnPageLoadDesc": { - "message": "You can turn off auto-fill on page load for individual login items from the item's Edit view." + "message": "You can turn off autofill on page load for individual login items from the item's Edit view." }, "itemAutoFillOnPageLoad": { - "message": "Auto-fill on page load (if set up in Options)" + "message": "Autofill on page load (if set up in Options)" }, "autoFillOnPageLoadUseDefault": { "message": "Use default setting" }, "autoFillOnPageLoadYes": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "autoFillOnPageLoadNo": { - "message": "Do not auto-fill on page load" + "message": "Do not autofill on page load" }, "commandOpenPopup": { "message": "Open vault popup" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Open vault in sidebar" }, - "commandAutofillDesc": { - "message": "Auto-fill the last used login for the current website" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Generate and copy a new random password to the clipboard" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Linked", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -1533,6 +1742,10 @@ "message": "Base domain", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domain name", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Match detection", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Default match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Toggle options" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "There are no passwords to list." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Remove" }, @@ -1677,6 +1899,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" }, @@ -1716,16 +1961,16 @@ "message": "Timeout action confirmation" }, "autoFillAndSave": { - "message": "Auto-fill and save" + "message": "Autofill and save" }, "fillAndSave": { "message": "Fill and save" }, "autoFillSuccessAndSavedUri": { - "message": "Item auto-filled and URI saved" + "message": "Item autofilled and URI saved" }, "autoFillSuccess": { - "message": "Item auto-filled " + "message": "Item autofilled " }, "insecurePageWarning": { "message": "Warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page." @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ is not a valid domain", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Password protected" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Copy Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send created", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2606,10 +2906,10 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { - "message": "How to auto-fill" + "message": "How to autofill" }, "autofillSelectInfoWithCommand": { "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", @@ -2627,16 +2927,25 @@ "message": "Got it" }, "autofillSettings": { - "message": "Auto-fill settings" + "message": "Autofill settings" + }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 8e6861e1b6d..37486833e94 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Logg på eller opprett en ny konto for å få tilgang til ditt sikre hvelv." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "Opprett en konto" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "Logg inn" - }, "enterpriseSingleSignOn": { "message": "Bedriftsinnlogging (SSO)" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Et hint for hovedpassordet (valgfritt)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "Fane" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Kopier sikkerhetskoden" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "Auto-utfylling" }, @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "Minimum passordlengde" }, "uppercase": { - "message": "Store bokstaver (A–Å)" + "message": "Store bokstaver (A–Å)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Små bokstaver (a-z)" + "message": "Små bokstaver (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Tall (0-9)" + "message": "Tall (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Spesialtegn (!@#$%^&*)" + "message": "Spesialtegn (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Antall ord" @@ -376,7 +455,12 @@ "message": "Minste antall spesialtegn" }, "avoidAmbChar": { - "message": "Unngå tvetydige tegn" + "message": "Unngå tvetydige tegn", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Søk i hvelvet" @@ -556,6 +640,18 @@ "security": { "message": "Sikkerhet" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "En feil har oppstått" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Din nye konto har blitt opprettet! Du kan nå logge på." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "En verifiseringskode er påkrevd." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Ugyldig bekreftelseskode" }, @@ -613,7 +718,7 @@ } }, "autofillError": { - "message": "Klarte ikke å auto-utfylle den valgte gjenstanden på denne siden. Kopier og lim inn informasjonen i stedet." + "message": "Klarte ikke å auto-utfylle det valgte elementet på denne siden. Kopier og lim inn informasjonen i stedet." }, "totpCaptureError": { "message": "Unable to scan QR code from the current webpage" @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Din innloggingsøkt har utløpt." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Er du sikker på at du vil logge av?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Ny URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "La til element" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Spør om å legge til innlogging" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "\"Legg til innlogging\"-beskjeden ber deg automatisk om å lagre nye innlogginger til hvelvet ditt hver gang du logger på dem for første gang." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Vis kort på fanesiden" }, "showCardsCurrentTabDesc": { "message": "Vis kortelementer på fanesiden for lett auto-utfylling." }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" + }, "showIdentitiesCurrentTab": { "message": "Vis identiteter på fanesiden" }, @@ -791,7 +936,7 @@ "message": "Ja, oppdater nå" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Lås opp" @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Standard URI-samsvarsgjenkjenning", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Velg standardmåten for å håndtere URI-samsvarsgjenkjenning for pålogginger ved f. eks. auto-utfylling." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB med kryptert fillagring for filvedlegg." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Du kan kjøpe et Premium-medlemskap på bitwarden.com. Vil du besøke det nettstedet nå?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Du er et Premium-medlem!" }, "premiumCurrentMemberThanks": { "message": "Takk for at du støtter Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "Og alt det for %price%/år!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Oppfriskning fullført" }, @@ -1178,14 +1341,23 @@ "message": "Miljø-nettadressene har blitt lagret." }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Rediger nettleserinnstillingene." @@ -1199,18 +1371,37 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "Aktiver auto-utfylling ved sideinnlastning" }, "enableAutoFillOnPageLoadDesc": { "message": "Dersom et innloggingskjema blir oppdaget, utfør automatisk en auto-utfylling når nettstedet lastes inn." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Kompromitterte eller upålitelige nettsider kan utnytte auto-utfylling når du laster inn siden." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" + }, "learnMoreAboutAutofill": { "message": "Lær mer om auto-utfylling" }, @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Åpne hvelv i sidepanelet" }, - "commandAutofillDesc": { - "message": "Auto-utfyll den senest brukte innloggingen til den nåværende nettsiden." + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Generer og kopier et nytt tilfeldig passord til utklippstavlen." @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolsk verdi" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Tilkoblet", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Passordhistorikk" }, @@ -1533,6 +1742,10 @@ "message": "Grunndomene", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domenenavn", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Match-gjenkjenning", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Standard match-gjenkjenning", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Skru innstillinger av/på" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Det er ingen passord å liste opp." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Fjern" }, @@ -1677,6 +1899,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" }, @@ -1734,7 +1979,7 @@ "message": "Ønsker du likevel å fylle ut denne innloggingen?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Det nye hovedpassordet ditt oppfyller ikke vilkår i virksomhetsreglene." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometri mislyktes" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Ekskluderte domener" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ er ikke et gyldig domene", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Passord beskyttet" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Kopier Send-lenke", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Opprettet Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Redigerte Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "E-postbekreftelse kreves" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Du må bekrefte e-posten din for å bruke denne funksjonen. Du kan bekrefte e-postadressen din i netthvelvet." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatisk registrering" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Innstillinger for auto-utfylling" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" + }, "autofillShortcut": { "message": "Auto-utfyll tastatursnarvei" }, - "autofillShortcutNotSet": { - "message": "Snarveien for automatisk utfylling er ikke angitt. Endre dette i nettleserens innstillinger." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "Snarveien for automatisk utfylling er: $COMMAND$. Endre dette i nettleserens innstillinger.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Inndata er påkrevd." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Velg --" }, @@ -2878,12 +3207,12 @@ "message": "Alias-domene" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Hopp frem til innholdet" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Lås opp kontoen", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Legg til nytt hvelvobjekt", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Lær mer om importalternativene dine" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "Du har ikke en samsvarende innlogging for dette nettstedet." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Bekreft" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Vanlige formater", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 95ec45da720..338d70eaf58 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "Create account" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "Log in" - }, "enterpriseSingleSignOn": { "message": "Enterprise single sign-on" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Master password hint (optional)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "Tab" }, @@ -107,17 +113,32 @@ "copySecurityCode": { "message": "Copy security code" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { - "message": "Auto-fill" + "message": "Autofill" }, "autoFillLogin": { - "message": "Auto-fill login" + "message": "Autofill login" }, "autoFillCard": { - "message": "Auto-fill card" + "message": "Autofill card" }, "autoFillIdentity": { - "message": "Auto-fill identity" + "message": "Autofill identity" }, "generatePasswordCopied": { "message": "Generate password (copied)" @@ -150,7 +171,7 @@ "message": "Log in to your vault" }, "autoFillInfo": { - "message": "There are no logins available to auto-fill for the current browser tab." + "message": "There are no logins available to autofill for the current browser tab." }, "addLogin": { "message": "Add a login" @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "Minimum password length" }, "uppercase": { - "message": "Uppercase (A-Z)" + "message": "Uppercase (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Lowercase (a-z)" + "message": "Lowercase (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numbers (0-9)" + "message": "Numbers (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Special characters (!@#$%^&*)" + "message": "Special characters (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Number of words" @@ -376,7 +455,12 @@ "message": "Minimum special" }, "avoidAmbChar": { - "message": "Avoid ambiguous characters" + "message": "Avoid ambiguous characters", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Search vault" @@ -556,6 +640,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -613,7 +718,7 @@ } }, "autofillError": { - "message": "Unable to auto-fill the selected item on this page. Copy and paste the information instead." + "message": "Unable to autofill the selected item on this page. Copy and paste the information instead." }, "totpCaptureError": { "message": "Unable to scan QR code from the current webpage" @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Your login session has expired." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "New URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "Item added" }, @@ -737,23 +873,32 @@ "enableAddLoginNotification": { "message": "Ask to add login" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "Ask to add an item if one isn't found in your vault." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Show cards on Tab page" }, "showCardsCurrentTabDesc": { - "message": "List card items on the Tab page for easy auto-fill." + "message": "List card items on the Tab page for easy autofill." + }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" }, "showIdentitiesCurrentTabDesc": { - "message": "List identity items on the Tab page for easy auto-fill." + "message": "List identity items on the Tab page for easy autofill." }, "clearClipboard": { "message": "Clear clipboard", @@ -791,7 +936,7 @@ "message": "Update" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Unlock" @@ -810,10 +955,10 @@ }, "defaultUriMatchDetection": { "message": "Default URI match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { - "message": "Choose the default way that URI match detection is handled for logins when performing actions such as auto-fill." + "message": "Choose the default way that URI match detection is handled for logins when performing actions such as autofill." }, "theme": { "message": "Theme" @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a Premium member!" }, "premiumCurrentMemberThanks": { "message": "Thank you for supporting Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "All for just $PRICE$ /year!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Refresh complete" }, @@ -1028,7 +1191,7 @@ "message": "Copy TOTP automatically" }, "disableAutoTotpCopyDesc": { - "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login." + "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you autofill the login." }, "enableAutoBiometricsPrompt": { "message": "Ask for biometrics on launch" @@ -1178,14 +1341,23 @@ "message": "Environment URLs saved" }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,38 +1371,57 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "enableAutoFillOnPageLoadDesc": { - "message": "If a login form is detected, auto-fill when the web page loads." + "message": "If a login form is detected, autofill when the web page loads." + }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Compromised or untrusted websites can exploit autofill on page load." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" }, "learnMoreAboutAutofill": { - "message": "Learn more about auto-fill" + "message": "Learn more about autofill" }, "defaultAutoFillOnPageLoad": { "message": "Default autofill setting for login items" }, "defaultAutoFillOnPageLoadDesc": { - "message": "You can turn off auto-fill on page load for individual login items from the item's Edit view." + "message": "You can turn off autofill on page load for individual login items from the item's Edit view." }, "itemAutoFillOnPageLoad": { - "message": "Auto-fill on page load (if set up in Options)" + "message": "Autofill on page load (if set up in Options)" }, "autoFillOnPageLoadUseDefault": { "message": "Use default setting" }, "autoFillOnPageLoadYes": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "autoFillOnPageLoadNo": { - "message": "Do not auto-fill on page load" + "message": "Do not autofill on page load" }, "commandOpenPopup": { "message": "Open vault popup" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Open vault in sidebar" }, - "commandAutofillDesc": { - "message": "Auto-fill the last used login for the current website" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Generate and copy a new random password to the clipboard" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Linked", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -1533,6 +1742,10 @@ "message": "Base domain", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domain name", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Match detection", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Default match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Toggle options" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "There are no passwords to list." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Remove" }, @@ -1677,6 +1899,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" }, @@ -1716,16 +1961,16 @@ "message": "Timeout action confirmation" }, "autoFillAndSave": { - "message": "Auto-fill and save" + "message": "Autofill and save" }, "fillAndSave": { "message": "Fill and save" }, "autoFillSuccessAndSavedUri": { - "message": "Item auto-filled and URI saved" + "message": "Item autofilled and URI saved" }, "autoFillSuccess": { - "message": "Item auto-filled " + "message": "Item autofilled " }, "insecurePageWarning": { "message": "Warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page." @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ is not a valid domain", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Password protected" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Copy Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send created", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2606,10 +2906,10 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { - "message": "How to auto-fill" + "message": "How to autofill" }, "autofillSelectInfoWithCommand": { "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", @@ -2627,16 +2927,25 @@ "message": "Got it" }, "autofillSettings": { - "message": "Auto-fill settings" + "message": "Autofill settings" + }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 79efae5fab9..435df8ecb4e 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -7,12 +7,15 @@ "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": "Thuis, op werk of onderweg. Bitwarden beveiligt makkelijk all je wachtwoorden, passkeys en gevoelige informatie", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in of maak een nieuw account aan om toegang te krijgen tot je beveiligde kluis." }, + "inviteAccepted": { + "message": "Uitnodiging geaccepteerd" + }, "createAccount": { "message": "Account aanmaken" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Rond het aanmaken van je account af met het instellen van een wachtwoord" }, - "login": { - "message": "Inloggen" - }, "enterpriseSingleSignOn": { "message": "Single sign-on voor bedrijven" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Hoofdwachtwoordhint (optioneel)" }, + "joinOrganization": { + "message": "Lid van organisatie worden" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Voltooi je lidmaatschap aan deze organisatie door een hoofdwachtwoord in te stellen." + }, "tab": { "message": "Tab" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Beveiligingscode kopiëren" }, + "copyName": { + "message": "Naam kopiëren" + }, + "copyCompany": { + "message": "Bedrijf kopiëren" + }, + "copySSN": { + "message": "Burgerservicenummer kopiëren" + }, + "copyPassportNumber": { + "message": "Paspoortnummer kopiëren" + }, + "copyLicenseNumber": { + "message": "Kenteken kopiëren" + }, "autoFill": { "message": "Auto-invullen" }, @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "Minimale wachtwoordlengte" }, "uppercase": { - "message": "Hoofdletters (A-Z)" + "message": "Hoofdletters (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Kleine letters (a-z)" + "message": "Kleine letters (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Cijfers (0-9)" + "message": "Cijfers (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Speciale tekens (!@#$%^&*)" + "message": "Speciale tekens (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Toevoegen", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Hoofdletters toevoegen", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Kleine letters toevoegen", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Nummers toevoegen", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Speciale tekens toevoegen", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Aantal woorden" @@ -376,7 +455,12 @@ "message": "Minimum aantal speciale tekens" }, "avoidAmbChar": { - "message": "Dubbelzinnige tekens vermijden" + "message": "Dubbelzinnige tekens vermijden", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Dubbelzinnige tekens vermijden", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Kluis doorzoeken" @@ -460,7 +544,7 @@ "message": "Stel een ontgrendelingsmethode in om je kluis time-out actie te wijzigen." }, "unlockMethodNeeded": { - "message": "Set up an unlock method in Settings" + "message": "Stel je ontgrendelingsmethode in in de instellingen" }, "sessionTimeoutHeader": { "message": "Sessietime-out" @@ -556,6 +640,18 @@ "security": { "message": "Beveiliging" }, + "confirmMasterPassword": { + "message": "Hoofdwachtwoord bevestigen" + }, + "masterPassword": { + "message": "Hoofdwachtwoord" + }, + "masterPassImportant": { + "message": "Je kunt je hoofdwachtwoord niet herstellen als je het vergeet!" + }, + "masterPassHintLabel": { + "message": "Hoofdwachtwoordhint" + }, "errorOccurred": { "message": "Er is een fout opgetreden" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Je nieuwe account is aangemaakt! Je kunt nu inloggen." }, + "newAccountCreated2": { + "message": "Je nieuwe account is aangemaakt!" + }, + "youHaveBeenLoggedIn": { + "message": "Je bent ingelogd!" + }, "youSuccessfullyLoggedIn": { "message": "U bent succesvol ingelogd" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Verificatiecode is vereist." }, + "webauthnCancelOrTimeout": { + "message": "De authenticatie werd geannuleerd of duurde te lang. Probeer het opnieuw." + }, "invalidVerificationCode": { "message": "Ongeldige verificatiecode" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan de authenticatie-QR-code van de huidige webpagina" }, + "totpHelperTitle": { + "message": "Maak tweestapsaanmelding naadloos" + }, + "totpHelper": { + "message": "Bitwarden kan tweestapsaanmeldingscodes opslaan en invullen. Kopieer en plak de sleutel in dit veld." + }, + "totpHelperWithCapture": { + "message": "Bitwarden kan tweestapsaanmeldingscodes opslaan en invullen. Selecteer het camerapictogram om een schermafbeelding van de QR-code van deze website te maken of kopieer en plak de sleutel in dit veld." + }, + "learnMoreAboutAuthenticators": { + "message": "Meer informatie over authenticatoren" + }, "copyTOTP": { "message": "Authenticatie-sleutel (TOTP) kopiëren" }, @@ -631,11 +748,26 @@ "message": "Uitgelogd" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "Je bent uitgelogd van je account." }, "loginExpired": { "message": "Je inlogsessie is verlopen." }, + "logIn": { + "message": "Inloggen" + }, + "restartRegistration": { + "message": "Registratie herstarten" + }, + "expiredLink": { + "message": "Verlopen link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Herstart de registratie of probeer in te loggen." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Je hebt al een account" + }, "logOutConfirmation": { "message": "Weet je zeker dat je wilt uitloggen?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Nieuwe URI" }, + "addDomain": { + "message": "Domein toevoegen", + "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": { "message": "Item is toegevoegd" }, @@ -737,11 +873,17 @@ "enableAddLoginNotification": { "message": "Vraag om toevoegen login" }, + "vaultSaveOptionsTitle": { + "message": "Opslaan in kluisopties" + }, "addLoginNotificationDesc": { "message": "\"Melding bij nieuwe login\" vraagt automatisch om nieuwe sites in de kluis op te slaan wanneer je ergens voor de eerste keer inlogt." }, "addLoginNotificationDescAlt": { - "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." + "message": "Vraag om een item toe te voegen als het niet is gevonden is je kluis. Dit geld voor alle ingelogde accounts." + }, + "showCardsInVaultView": { + "message": "Kaarten als Autofill-suggesties in de kluisweergave weergeven" }, "showCardsCurrentTab": { "message": "Kaarten weergeven op tabpagina" @@ -749,6 +891,9 @@ "showCardsCurrentTabDesc": { "message": "Kaartenitems weergeven op de tabpagina voor gemakkelijk automatisch invullen." }, + "showIdentitiesInVaultView": { + "message": "Identiteiten als Autofill-suggesties in de kluisweergave weergeven" + }, "showIdentitiesCurrentTab": { "message": "Identiteiten weergeven op tabpagina" }, @@ -776,7 +921,7 @@ "message": "Vraag om bijwerken van het wachtwoord van een login zodra een wijziging op een website is gedetecteerd." }, "changedPasswordNotificationDescAlt": { - "message": "Ask to update a login's password when a change is detected on a website. Applies to all logged in accounts." + "message": "Vraag om het wachtwoord bij te werken als er een wijziging is gedetecteerd op een website. Geldt voor alle ingelogde accounts." }, "enableUsePasskeys": { "message": "Vragen om opslaan en gebruiken van passkeys en wachtwoorden" @@ -806,11 +951,11 @@ "message": "Gebruik de tweede klikfunctie voor toegang tot wachtwoordgeneratie en het matchen van logins voor de website." }, "contextMenuItemDescAlt": { - "message": "Use a secondary click to access password generation and matching logins for the website. Applies to all logged in accounts." + "message": "Gebruik de tweede klikfunctie voor toegang tot wachtwoordgeneratie en het matchen van logins voor de website." }, "defaultUriMatchDetection": { "message": "Standaard URI-overeenkomstdetectie", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Kies de standaardmethode voor detectie van URI-overeenkomsten voor logins bij acties, zoals automatisch invullen." @@ -822,7 +967,7 @@ "message": "Het kleurenthema van de toepassing wijzigen." }, "themeDescAlt": { - "message": "Change the application's color theme. Applies to all logged in accounts." + "message": "Verander het kleurenthema van de applicatie. Geldt voor alle ingelogde accounts." }, "dark": { "message": "Donker", @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB versleutelde opslag voor bijlagen." }, + "premiumSignUpEmergency": { + "message": "Noodtoegang" + }, "premiumSignUpTwoStepOptions": { "message": "Eigen opties voor tweestapsaanmelding zoals YubiKey en Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Je kunt een Premium-abonnement aanschaffen in de webkluis op bitwarden.com. Wil je de website nu bezoeken?" }, + "premiumPurchaseAlertV2": { + "message": "Je kunt Premium via je accountinstellingen in de Bitwarden-webapp kopen." + }, "premiumCurrentMember": { "message": "Je bent Premium-lid!" }, "premiumCurrentMemberThanks": { "message": "Bedankt voor het ondersteunen van Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade naar premium en ontvang:" + }, "premiumPrice": { "message": "Dit alles voor slechts $PRICE$ per jaar!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "Dit alles voor slechts $PRICE$ per jaar!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Bijwerken voltooid" }, @@ -1142,7 +1305,7 @@ "message": "Geef de basis-URL van jouw zelfgehoste Bitwarden-installatie." }, "selfHostedBaseUrlHint": { - "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" + "message": "Specificeer de basis-URL van je zelfgehoste Bitwarden-installatie. Bijvoorbeeld: https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { "message": "For advanced configuration, you can specify the base URL of each service independently." @@ -1179,10 +1342,19 @@ }, "showAutoFillMenuOnFormFields": { "message": "Auto-invulmenu op formuliervelden weergeven", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { - "message": "Applies to all logged in accounts." + "autofillSuggestionsSectionTitle": { + "message": "Suggesties voor automatisch invullen" + }, + "showInlineMenuLabel": { + "message": "Suggesties voor automatisch invullen op formuliervelden weergeven" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Suggesties weergeven wanneer pictogram is geselecteerd" + }, + "showInlineMenuOnFormFieldsDescAlt": { + "message": "Van toepassing op alle ingelogde accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { "message": "Schakel de ingebouwde wachtwoordbeheerinstellingen van je browser uit om conflicten te voorkomen." @@ -1202,15 +1374,34 @@ "message": "Wanneer het pictogram automatisch aanvullen is geselecteerd", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Automatisch invullen bij laden van pagina" + }, "enableAutoFillOnPageLoad": { "message": "Automatisch invullen bij laden van pagina" }, "enableAutoFillOnPageLoadDesc": { "message": "Als een inlogformulier wordt gedetecteerd, dan worden de inloggegevens automatisch ingevuld." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Waarschuwing:$CLOSETAG$ Geconpromitteerde of niet-vertrouwde websites kunnen het automatische invullen bij het laden van de pagina misbruiken.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Gehackte of onbetrouwbare websites kunnen auto-invullen tijdens het laden van de pagina misbruiken." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Meer over risico's lezen" + }, "learnMoreAboutAutofill": { "message": "Lees meer over automatisch invullen" }, @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Open kluis in zijbalk" }, - "commandAutofillDesc": { - "message": "Vul automatisch de laatst gebruikte login in voor de huidige website" + "commandAutofillLoginDesc": { + "message": "Automatisch de laatst gebruikte login invullen voor de huidige website" + }, + "commandAutofillCardDesc": { + "message": "Automatisch de laatst gebruikte kaart invullen voor de huidige website" + }, + "commandAutofillIdentityDesc": { + "message": "Automatisch de laatst gebruikte identiteit invullen voor de huidige website" }, "commandGeneratePasswordDesc": { "message": "Genereer en kopieer een nieuw willekeurig wachtwoord naar het klembord." @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Selectievakje" + }, "cfTypeLinked": { "message": "Gekoppeld", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "$TYPE$ weergeven", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Geschiedenis" }, @@ -1533,6 +1742,10 @@ "message": "Basisdomein", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Basisdomein (aanbevolen)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domeinnaam", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Overeenkomstdetectie", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Standaard overeenkomstdetectie", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Opties schakelen" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Er zijn geen wachtwoorden om weer te geven." }, + "clearHistory": { + "message": "Geschiedenis wissen" + }, + "noPasswordsToShow": { + "message": "Geen wachtwoorden weer te geven" + }, + "noRecentlyGeneratedPassword": { + "message": "Je hebt onlangs geen wachtwoord gegenereerd" + }, "remove": { "message": "Verwijderen" }, @@ -1651,7 +1873,7 @@ "message": "Ongeldige PIN-code." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "PIN te vaak verkeerd ingevuld. Bezig met uitloggen." }, "unlockWithBiometrics": { "message": "Biometrisch ontgrendelen" @@ -1677,6 +1899,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" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Je nieuwe hoofdwachtwoord voldoet niet aan de beleidseisen." }, - "receiveMarketingEmails": { - "message": "Ontvang e-mailberichten van Bitwarden voor aankondigingen, advies en onderzoeksmogelijkheden." + "receiveMarketingEmailsV2": { + "message": "Krijg advies, aankondigingen en onderzoeksmogelijkheden van Bitwarden in je inbox." }, "unsubscribe": { "message": "Afmelden" @@ -1833,7 +2078,7 @@ "message": "Ok" }, "errorRefreshingAccessToken": { - "message": "Access Token Refresh Error" + "message": "Fout bij vernieuwen toegangstoken" }, "errorRefreshingAccessTokenDesc": { "message": "No refresh token or API keys found. Please try logging out and logging back in." @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometrisch ontgrendelen niet beschikbaar" + }, + "biometricsNotAvailableDesc": { + "message": "Biometrische ontgrendeling is momenteel niet beschikbaar. Probeer het later opnieuw." + }, "biometricsFailedTitle": { "message": "Biometrie mislukt" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "Organisatiebeleid heeft het importeren van items in je persoonlijke kluis geblokkeerd." }, + "domainsTitle": { + "message": "Domeinen", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Uitgesloten domeinen" }, @@ -1926,17 +2187,29 @@ "message": "Bitwarden zal voor deze domeinen niet vragen om inloggegevens op te slaan. Je moet de pagina vernieuwen om de wijzigingen toe te passen." }, "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." + "message": "Bitwarden zal voor deze domeinen niet vragen om de wachtwoorden op te slaan voor alle ingelogde accounts. Je moet de pagina verversen om de wijzigingen op te slaan." + }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ is geen geldig domein", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Uitgesloten domeinwijzigingen opgeslagen" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Beveiligd met wachtwoord" }, + "copyLink": { + "message": "Link kopiëren" + }, "copySendLink": { "message": "Send-link kopiëren", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send aangemaakt", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send succesvol aangemaakt!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "De Send is de komende $DAYS$ dagen beschikbaar voor iedereen met de link.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send-link gekopieerd", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send bewerkt", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "E-mailverificatie vereist" }, + "emailVerifiedV2": { + "message": "E-mailadres geverifieerd" + }, "emailVerificationRequiredDesc": { "message": "Je moet je e-mailadres verifiëren om deze functie te gebruiken. Je kunt je e-mailadres verifiëren in de kluis." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatische inschrijving" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Instellingen automatisch invullen" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Shortcut voor automatisch invullen" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Snelkoppeling wijzigen" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Snelkoppelingen beheren" + }, "autofillShortcut": { "message": "Snelkoppeling automatisch invullen" }, - "autofillShortcutNotSet": { - "message": "De sneltoets voor automatisch invullen is niet ingesteld. Wijzig dit in de instellingen van de browser." + "autofillLoginShortcutNotSet": { + "message": "De sneltoets voor automatisch invullen is niet ingesteld. Je kunt dit in de instellingen van de browser wijzigen." }, - "autofillShortcutText": { - "message": "De sneltoets voor automatisch invullen is: $COMMAND$. Wijzig dit in de instellingen van de browser.", + "autofillLoginShortcutText": { + "message": "De sneltoets voor automatisch invullen is $COMMAND$. Je kunt alle sneltoetsen in de instellingen van de browser wijzigen.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Vertrouwd apparaat" }, + "sendsNoItemsTitle": { + "message": "Geen actieve Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Gebruik Send voor het veilig delen van versleutelde informatie met wie dan ook.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Invoer vereist." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 veld heeft je aandacht nodig." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ velden hebben je aandacht nodig.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Selecteer --" }, @@ -2878,12 +3207,12 @@ "message": "Aliasdomein" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2911,10 +3240,18 @@ "message": "Ontgrendel je account om overeenkomende logins te bekijken", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Je account ontgrendelen om suggesties voor automatisch inloggen te bekijken", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Account ontgrendelen", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Je account ontgrendelen, opent in een nieuw venster", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Inloggegevens invullen voor", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Nieuwe kluisitem toevoegen", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "Nieuwe login", + "description": "Button text to display within inline menu when there are no matching items on a login field" + }, + "addNewLoginItemAria": { + "message": "Nieuw inlogitem aan kluis toevoegen, openen in een nieuw venster", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "Nieuwe kaart", + "description": "Button text to display within inline menu when there are no matching items on a credit card field" + }, + "addNewCardItemAria": { + "message": "Nieuw item aan kluis toevoegen, openen in een nieuw venster", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "Nieuwe identiteit", + "description": "Button text to display within inline menu when there are no matching items on an identity field" + }, + "addNewIdentityItemAria": { + "message": "Nieuw identiteitsitem aan kluis toevoegen, openen in een nieuw venster", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Bitwarden auto-invulmenu beschikbaar. Druk op de pijltjestoets omlaag om te selecteren.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Fout bij het verbinden met de Duo-service. Gebruik een andere tweestapsaanmeldingsmethode of neem contact op met Duo voor hulp." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Onjuist bestandswachtwoord, gebruik het wachtwoord dat je hebt ingevoerd bij het aanmaken van het exportbestand." }, - "importDestination": { - "message": "Importbestemming" + "destination": { + "message": "Bestemming" }, "learnAboutImportOptions": { "message": "Leer meer over je importopties" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "De initiërende site vereist verificatie. Deze functie is nog niet geïmplementeerd voor accounts zonder hoofdwachtwoord." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Inloggen met passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "Je hebt geen overeenkomende login voor deze site." }, + "noMatchingLoginsForSite": { + "message": "Geen overeenkomende logins voor deze site" + }, "confirm": { "message": "Bevestigen" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Passkey als nieuwe login opslaan" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Kies een login om deze passkey bij op te slaan" }, + "chooseCipherForPasskeyAuth": { + "message": "Kies een passkey om mee in te loggen" + }, "passkeyItem": { "message": "Passkey-Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,6 +3659,30 @@ "message": "Veelvoorkomende formaten", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Doorgaan naar browserinstellingen?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Doorgaan naar Helpcentrum?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" + }, + "confirmContinueToHelpCenterPasswordManagementContent": { + "message": "De instellingen voor het beheer van je browser's automatisch invullen en wachtwoorden veranderen.", + "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": "Je kunt sneltoetsen van de extensie bekijken en instellen in de instellingen van je browser.", + "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": "De instellingen voor het beheer van je browser's automatisch invullen en wachtwoorden veranderen.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" + }, + "confirmContinueToBrowserKeyboardShortcutSettingsContent": { + "message": "Je kunt sneltoetsen van de extensie bekijken en instellen in de instellingen van je browser.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Bitwarden als je standaardwachtbeheerder gebruiken?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Wachtwoord opgeslagen!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Wachtwoord bijgewerkt!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,20 +3736,6 @@ "passkeyRemoved": { "message": "Passkey verwijderd" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Suggesties voor automatisch invullen" }, @@ -3415,7 +3803,7 @@ }, "autofillTitle": { "message": "Automatisch invullen - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "Geen waarden om te kopiëren" }, - "assignCollections": { - "message": "Collecties toewijzen" + "assignToCollections": { + "message": "Aan collecties toewijzen" }, "copyEmail": { "message": "Copy email" @@ -3448,7 +3836,7 @@ "message": "Notifications" }, "appearance": { - "message": "Voorkomen" + "message": "Uiterlijk" }, "errorAssigningTargetCollection": { "message": "Fout bij toewijzen doelverzameling." @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in een gedeactiveerde organisatie zijn niet toegankelijk. Neem contact op met de eigenaar van je organisatie voor hulp." }, + "additionalInformation": { + "message": "Aanvullende informatie" + }, + "itemHistory": { + "message": "Itemgeschiedenis" + }, + "lastEdited": { + "message": "Laatst gewijzigd" + }, + "ownerYou": { + "message": "Eigenaar: Jij" + }, + "linked": { + "message": "Gekoppeld" + }, + "copySuccessful": { + "message": "Kopiëren gelukt" + }, "upload": { "message": "Uploaden" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Persoonlijke gegevens" + }, + "identification": { + "message": "Identificatie" + }, + "contactInfo": { + "message": "Contactgegevens" + }, + "downloadAttachment": { + "message": "$ITEMNAME$ downloaden", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "kaartnummer eindigt met", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Inloggegevens" + }, + "authenticatorKey": { + "message": "Authenticatiesleutel" + }, + "autofillOptions": { + "message": "Instellingen automatisch invullen" + }, + "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 toegevoegd" + }, + "addWebsite": { + "message": "Website toevoegen" + }, + "deleteWebsite": { + "message": "Website verwijderen" + }, + "defaultLabel": { + "message": "Standaard ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Overeenkomstdetectie weergeven $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Overeenkomstdetectie verbergen $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Automatisch invullen bij laden van pagina?" + }, + "cardExpiredTitle": { + "message": "Verlopen kaart" + }, + "cardExpiredMessage": { + "message": "Als je het hebt vernieuwd, werk dan de informatie van de kaart bij" + }, + "cardDetails": { + "message": "Kaartgegevens" + }, + "cardBrandDetails": { + "message": "", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Animaties inschakelen" + }, + "addAccount": { + "message": "Account toevoegen" + }, + "loading": { + "message": "Laden" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Alleen organisatieleden met toegang tot deze collecties kunnen de items zien." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Alleen organisatieleden met toegang tot deze collecties kunnen de items zien." + }, + "bulkCollectionAssignmentWarning": { + "message": "Je hebt $TOTAL_COUNT$ items geselecteerd. Je kunt $READONLY_COUNT$ items niet bijwerken omdat je geen bewerkrechten hebt.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Veld toevoegen" + }, + "add": { + "message": "Toevoegen" + }, + "fieldType": { + "message": "Veldtype" + }, + "fieldLabel": { + "message": "Veldlabel" + }, + "textHelpText": { + "message": "Gebruik tekstvelden voor data zoals beveiligingsvragen" + }, + "hiddenHelpText": { + "message": "Gebruik verborgen velden voor gevoelige gegevens zoals een wachtwoord" + }, + "checkBoxHelpText": { + "message": "Gebruik aanvinkvakjes als je een formulier automatisch wilt invullen, zoals e-mailadres herinneren" + }, + "linkedHelpText": { + "message": "Gebruik een gekoppeld veld als je problemen ervaart met het automatisch invullen voor een specifieke website." + }, + "linkedLabelHelpText": { + "message": "Html-id, naam, aria-label of placeholder van het veld invullen." + }, + "editField": { + "message": "Veld bewerken" + }, + "editFieldLabel": { + "message": "$LABEL$ bewerken", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "$LABEL$ verwijderen", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ toegevoegd", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "$LABEL$ herschikken. Gebruik de pijltjestoets om het item omhoog of omlaag te verplaatsen.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ is naar boven verplaatst, positie $INDEX$ van $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Collecties voor toewijzen selecteren" + }, + "personalItemTransferWarningSingular": { + "message": "1 item wordt permanent overgedragen aan de geselecteerde organisatie. Je bent niet langer de eigenaar van dit item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items worden permanent overgedragen aan de geselecteerde organisatie. Je bent niet langer de eigenaar van deze items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item wordt permanent overgedragen aan $ORG$. Je bent niet langer de eigenaar van dit item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items worden permanent overgedragen aan $ORG$. Je bent niet langer de eigenaar van deze items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Succesvol toegewezen collecties" + }, + "nothingSelected": { + "message": "Je hebt niets geselecteerd." + }, + "movedItemsToOrg": { + "message": "Geselecteerde items verplaatst naar $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items verplaatst naar $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Items verplaatst naar $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ is naar boven verplaatst, positie $INDEX$ van $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Itemlocatie" + }, + "fileSends": { + "message": "Bestand-Sends" + }, + "textSends": { + "message": "Tekst-Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden heeft een nieuw uiterlijk!" + }, + "bitwardenNewLookDesc": { + "message": "Automatisch invullen en zoeken is makkelijker en intuïtiever dan ooit vanaf het tabblad Kluis. Kijk rond!" + }, + "accountActions": { + "message": "Accountacties" + }, + "showNumberOfAutofillSuggestions": { + "message": "Aantal login-autofill-suggesties op het extensie-pictogram weergeven" + }, + "systemDefault": { + "message": "Systeemstandaard" + }, + "enterprisePolicyRequirementsApplied": { + "message": "Bedrijfsbeleidseisen zijn op deze instelling toegepast" + }, + "fileSavedToDevice": { + "message": "Bestand op apparaat opgeslagen. Beheer vanaf de downloads op je apparaat." + }, + "showCharacterCount": { + "message": "Aantal tekens weergeven" + }, + "hideCharacterCount": { + "message": "Aantal tekens verbergen" + }, + "itemsInTrash": { + "message": "Items in prullenbak" + }, + "noItemsInTrash": { + "message": "Geen items in prullenbak" + }, + "noItemsInTrashDesc": { + "message": "Items die je verwijdert verschijnen hier en worden na 30 dagen definitief verwijderd" + }, + "trashWarning": { + "message": "Items die meer dan 30 dagen in de prullenbak zitten worden automatisch verwijderd" + }, + "restore": { + "message": "Herstellen" + }, + "deleteForever": { + "message": "Definitief verwijderen" + }, + "noEditPermissions": { + "message": "Je hebt geen toestemming om dit item te bewerken" } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 95ec45da720..338d70eaf58 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "Create account" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "Log in" - }, "enterpriseSingleSignOn": { "message": "Enterprise single sign-on" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Master password hint (optional)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "Tab" }, @@ -107,17 +113,32 @@ "copySecurityCode": { "message": "Copy security code" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { - "message": "Auto-fill" + "message": "Autofill" }, "autoFillLogin": { - "message": "Auto-fill login" + "message": "Autofill login" }, "autoFillCard": { - "message": "Auto-fill card" + "message": "Autofill card" }, "autoFillIdentity": { - "message": "Auto-fill identity" + "message": "Autofill identity" }, "generatePasswordCopied": { "message": "Generate password (copied)" @@ -150,7 +171,7 @@ "message": "Log in to your vault" }, "autoFillInfo": { - "message": "There are no logins available to auto-fill for the current browser tab." + "message": "There are no logins available to autofill for the current browser tab." }, "addLogin": { "message": "Add a login" @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "Minimum password length" }, "uppercase": { - "message": "Uppercase (A-Z)" + "message": "Uppercase (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Lowercase (a-z)" + "message": "Lowercase (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numbers (0-9)" + "message": "Numbers (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Special characters (!@#$%^&*)" + "message": "Special characters (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Number of words" @@ -376,7 +455,12 @@ "message": "Minimum special" }, "avoidAmbChar": { - "message": "Avoid ambiguous characters" + "message": "Avoid ambiguous characters", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Search vault" @@ -556,6 +640,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -613,7 +718,7 @@ } }, "autofillError": { - "message": "Unable to auto-fill the selected item on this page. Copy and paste the information instead." + "message": "Unable to autofill the selected item on this page. Copy and paste the information instead." }, "totpCaptureError": { "message": "Unable to scan QR code from the current webpage" @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Your login session has expired." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "New URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "Item added" }, @@ -737,23 +873,32 @@ "enableAddLoginNotification": { "message": "Ask to add login" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "Ask to add an item if one isn't found in your vault." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Show cards on Tab page" }, "showCardsCurrentTabDesc": { - "message": "List card items on the Tab page for easy auto-fill." + "message": "List card items on the Tab page for easy autofill." + }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" }, "showIdentitiesCurrentTabDesc": { - "message": "List identity items on the Tab page for easy auto-fill." + "message": "List identity items on the Tab page for easy autofill." }, "clearClipboard": { "message": "Clear clipboard", @@ -791,7 +936,7 @@ "message": "Update" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Unlock" @@ -810,10 +955,10 @@ }, "defaultUriMatchDetection": { "message": "Default URI match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { - "message": "Choose the default way that URI match detection is handled for logins when performing actions such as auto-fill." + "message": "Choose the default way that URI match detection is handled for logins when performing actions such as autofill." }, "theme": { "message": "Theme" @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a Premium member!" }, "premiumCurrentMemberThanks": { "message": "Thank you for supporting Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "All for just $PRICE$ /year!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Refresh complete" }, @@ -1028,7 +1191,7 @@ "message": "Copy TOTP automatically" }, "disableAutoTotpCopyDesc": { - "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login." + "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you autofill the login." }, "enableAutoBiometricsPrompt": { "message": "Ask for biometrics on launch" @@ -1178,14 +1341,23 @@ "message": "Environment URLs saved" }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,38 +1371,57 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "enableAutoFillOnPageLoadDesc": { - "message": "If a login form is detected, auto-fill when the web page loads." + "message": "If a login form is detected, autofill when the web page loads." + }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Compromised or untrusted websites can exploit autofill on page load." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" }, "learnMoreAboutAutofill": { - "message": "Learn more about auto-fill" + "message": "Learn more about autofill" }, "defaultAutoFillOnPageLoad": { "message": "Default autofill setting for login items" }, "defaultAutoFillOnPageLoadDesc": { - "message": "You can turn off auto-fill on page load for individual login items from the item's Edit view." + "message": "You can turn off autofill on page load for individual login items from the item's Edit view." }, "itemAutoFillOnPageLoad": { - "message": "Auto-fill on page load (if set up in Options)" + "message": "Autofill on page load (if set up in Options)" }, "autoFillOnPageLoadUseDefault": { "message": "Use default setting" }, "autoFillOnPageLoadYes": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "autoFillOnPageLoadNo": { - "message": "Do not auto-fill on page load" + "message": "Do not autofill on page load" }, "commandOpenPopup": { "message": "Open vault popup" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Open vault in sidebar" }, - "commandAutofillDesc": { - "message": "Auto-fill the last used login for the current website" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Generate and copy a new random password to the clipboard" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Linked", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -1533,6 +1742,10 @@ "message": "Base domain", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domain name", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Match detection", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Default match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Toggle options" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "There are no passwords to list." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Remove" }, @@ -1677,6 +1899,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" }, @@ -1716,16 +1961,16 @@ "message": "Timeout action confirmation" }, "autoFillAndSave": { - "message": "Auto-fill and save" + "message": "Autofill and save" }, "fillAndSave": { "message": "Fill and save" }, "autoFillSuccessAndSavedUri": { - "message": "Item auto-filled and URI saved" + "message": "Item autofilled and URI saved" }, "autoFillSuccess": { - "message": "Item auto-filled " + "message": "Item autofilled " }, "insecurePageWarning": { "message": "Warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page." @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ is not a valid domain", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Password protected" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Copy Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send created", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2606,10 +2906,10 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { - "message": "How to auto-fill" + "message": "How to autofill" }, "autofillSelectInfoWithCommand": { "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", @@ -2627,16 +2927,25 @@ "message": "Got it" }, "autofillSettings": { - "message": "Auto-fill settings" + "message": "Autofill settings" + }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 95ec45da720..338d70eaf58 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "Create account" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "Log in" - }, "enterpriseSingleSignOn": { "message": "Enterprise single sign-on" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Master password hint (optional)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "Tab" }, @@ -107,17 +113,32 @@ "copySecurityCode": { "message": "Copy security code" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { - "message": "Auto-fill" + "message": "Autofill" }, "autoFillLogin": { - "message": "Auto-fill login" + "message": "Autofill login" }, "autoFillCard": { - "message": "Auto-fill card" + "message": "Autofill card" }, "autoFillIdentity": { - "message": "Auto-fill identity" + "message": "Autofill identity" }, "generatePasswordCopied": { "message": "Generate password (copied)" @@ -150,7 +171,7 @@ "message": "Log in to your vault" }, "autoFillInfo": { - "message": "There are no logins available to auto-fill for the current browser tab." + "message": "There are no logins available to autofill for the current browser tab." }, "addLogin": { "message": "Add a login" @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "Minimum password length" }, "uppercase": { - "message": "Uppercase (A-Z)" + "message": "Uppercase (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Lowercase (a-z)" + "message": "Lowercase (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numbers (0-9)" + "message": "Numbers (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Special characters (!@#$%^&*)" + "message": "Special characters (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Number of words" @@ -376,7 +455,12 @@ "message": "Minimum special" }, "avoidAmbChar": { - "message": "Avoid ambiguous characters" + "message": "Avoid ambiguous characters", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Search vault" @@ -556,6 +640,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -613,7 +718,7 @@ } }, "autofillError": { - "message": "Unable to auto-fill the selected item on this page. Copy and paste the information instead." + "message": "Unable to autofill the selected item on this page. Copy and paste the information instead." }, "totpCaptureError": { "message": "Unable to scan QR code from the current webpage" @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Your login session has expired." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "New URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "Item added" }, @@ -737,23 +873,32 @@ "enableAddLoginNotification": { "message": "Ask to add login" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "Ask to add an item if one isn't found in your vault." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Show cards on Tab page" }, "showCardsCurrentTabDesc": { - "message": "List card items on the Tab page for easy auto-fill." + "message": "List card items on the Tab page for easy autofill." + }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" }, "showIdentitiesCurrentTabDesc": { - "message": "List identity items on the Tab page for easy auto-fill." + "message": "List identity items on the Tab page for easy autofill." }, "clearClipboard": { "message": "Clear clipboard", @@ -791,7 +936,7 @@ "message": "Update" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Unlock" @@ -810,10 +955,10 @@ }, "defaultUriMatchDetection": { "message": "Default URI match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { - "message": "Choose the default way that URI match detection is handled for logins when performing actions such as auto-fill." + "message": "Choose the default way that URI match detection is handled for logins when performing actions such as autofill." }, "theme": { "message": "Theme" @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a Premium member!" }, "premiumCurrentMemberThanks": { "message": "Thank you for supporting Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "All for just $PRICE$ /year!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Refresh complete" }, @@ -1028,7 +1191,7 @@ "message": "Copy TOTP automatically" }, "disableAutoTotpCopyDesc": { - "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login." + "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you autofill the login." }, "enableAutoBiometricsPrompt": { "message": "Ask for biometrics on launch" @@ -1178,14 +1341,23 @@ "message": "Environment URLs saved" }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,38 +1371,57 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "enableAutoFillOnPageLoadDesc": { - "message": "If a login form is detected, auto-fill when the web page loads." + "message": "If a login form is detected, autofill when the web page loads." + }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Compromised or untrusted websites can exploit autofill on page load." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" }, "learnMoreAboutAutofill": { - "message": "Learn more about auto-fill" + "message": "Learn more about autofill" }, "defaultAutoFillOnPageLoad": { "message": "Default autofill setting for login items" }, "defaultAutoFillOnPageLoadDesc": { - "message": "You can turn off auto-fill on page load for individual login items from the item's Edit view." + "message": "You can turn off autofill on page load for individual login items from the item's Edit view." }, "itemAutoFillOnPageLoad": { - "message": "Auto-fill on page load (if set up in Options)" + "message": "Autofill on page load (if set up in Options)" }, "autoFillOnPageLoadUseDefault": { "message": "Use default setting" }, "autoFillOnPageLoadYes": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "autoFillOnPageLoadNo": { - "message": "Do not auto-fill on page load" + "message": "Do not autofill on page load" }, "commandOpenPopup": { "message": "Open vault popup" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Open vault in sidebar" }, - "commandAutofillDesc": { - "message": "Auto-fill the last used login for the current website" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Generate and copy a new random password to the clipboard" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Linked", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -1533,6 +1742,10 @@ "message": "Base domain", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domain name", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Match detection", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Default match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Toggle options" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "There are no passwords to list." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Remove" }, @@ -1677,6 +1899,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" }, @@ -1716,16 +1961,16 @@ "message": "Timeout action confirmation" }, "autoFillAndSave": { - "message": "Auto-fill and save" + "message": "Autofill and save" }, "fillAndSave": { "message": "Fill and save" }, "autoFillSuccessAndSavedUri": { - "message": "Item auto-filled and URI saved" + "message": "Item autofilled and URI saved" }, "autoFillSuccess": { - "message": "Item auto-filled " + "message": "Item autofilled " }, "insecurePageWarning": { "message": "Warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page." @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ is not a valid domain", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Password protected" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Copy Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send created", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2606,10 +2906,10 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { - "message": "How to auto-fill" + "message": "How to autofill" }, "autofillSelectInfoWithCommand": { "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", @@ -2627,16 +2927,25 @@ "message": "Got it" }, "autofillSettings": { - "message": "Auto-fill settings" + "message": "Autofill settings" + }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index cb0948eb9cc..a99655378e5 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Zaloguj się lub utwórz nowe konto, aby uzyskać dostęp do Twojego bezpiecznego sejfu." }, + "inviteAccepted": { + "message": "Zaproszenie zostało zaakceptowane" + }, "createAccount": { "message": "Utwórz konto" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Ukończ tworzenie konta poprzez ustawienie hasła" }, - "login": { - "message": "Zaloguj się" - }, "enterpriseSingleSignOn": { "message": "Logowanie jednokrotne" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Podpowiedź do hasła głównego (opcjonalnie)" }, + "joinOrganization": { + "message": "Dołącz do organizacji" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Zakończ dołączanie do tej organizacji przez ustawienie hasła głównego." + }, "tab": { "message": "Karta" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Kopiuj kod zabezpieczający" }, + "copyName": { + "message": "Kopiuj nazwę" + }, + "copyCompany": { + "message": "Kopiuj firmę" + }, + "copySSN": { + "message": "Kopiuj numer PESEL" + }, + "copyPassportNumber": { + "message": "Kopiuj numer paszportu" + }, + "copyLicenseNumber": { + "message": "Kopiuj numer licencji" + }, "autoFill": { "message": "Autouzupełnianie" }, @@ -150,7 +171,7 @@ "message": "Zaloguj się do sejfu" }, "autoFillInfo": { - "message": "Brak dostępnych danych logowania do autouzupełnienia dla obecnej karty przeglądarki." + "message": "Brak danych logowania do użycia przez autouzupełnienie na obecnej karcie przeglądarki." }, "addLogin": { "message": "Dodaj dane logowania" @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "Minimalna długość hasła" }, "uppercase": { - "message": "Wielkie litery (A-Z)" + "message": "Wielkie litery (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Małe litery (a-z)" + "message": "Małe litery (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Cyfry (0-9)" + "message": "Cyfry (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Znaki specjalne (!@#$%^&*)" + "message": "Znaki specjalne (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Liczba słów" @@ -376,7 +455,12 @@ "message": "Minimalna liczba znaków specjalnych" }, "avoidAmbChar": { - "message": "Unikaj niejednoznacznych znaków" + "message": "Unikaj niejednoznacznych znaków", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Szukaj w sejfie" @@ -509,7 +593,7 @@ "message": "Zablokuj" }, "lockAll": { - "message": "Zablokuj wszystko" + "message": "Zablokuj wszystkie" }, "immediately": { "message": "Natychmiast" @@ -556,6 +640,18 @@ "security": { "message": "Zabezpieczenia" }, + "confirmMasterPassword": { + "message": "Potwierdź hasło główne" + }, + "masterPassword": { + "message": "Hasło główne" + }, + "masterPassImportant": { + "message": "Twoje hasło główne nie może zostać odzyskane, jeśli je zapomnisz!" + }, + "masterPassHintLabel": { + "message": "Podpowiedź do hasła głównego" + }, "errorOccurred": { "message": "Wystąpił błąd" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Konto zostało utworzone! Teraz możesz się zalogować." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "Zalogowałeś się pomyślnie" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Kod weryfikacyjny jest wymagany." }, + "webauthnCancelOrTimeout": { + "message": "Uwierzytelnianie zostało anulowane lub trwało zbyt długo. Spróbuj ponownie." + }, "invalidVerificationCode": { "message": "Kod weryfikacyjny jest nieprawidłowy" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Zeskanuj kod QR z bieżącej strony" }, + "totpHelperTitle": { + "message": "Spraw, aby dwuetapowa weryfikacja była bezproblemowa" + }, + "totpHelper": { + "message": "Bitwarden może przechowywać i wypełniać kody weryfikacyjne. Skopiuj i wklej klucz do tego pola." + }, + "totpHelperWithCapture": { + "message": "Bitwarden może przechowywać i wypełniać kody weryfikacyjne. Wybierz ikonę aparatu, aby zrobić zrzut ekranu z kodem QR lub skopiuj i wklej klucz do tego pola." + }, + "learnMoreAboutAuthenticators": { + "message": "Learn more about authenticators" + }, "copyTOTP": { "message": "Kopiuj klucz uwierzytelniający (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Twoja sesja wygasła." }, + "logIn": { + "message": "Zaloguj się" + }, + "restartRegistration": { + "message": "Zrestartuj rejestrację" + }, + "expiredLink": { + "message": "Link wygasł" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Zrestartuj rejestrację lub spróbuj się zalogować." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Możesz mieć już konto" + }, "logOutConfirmation": { "message": "Czy na pewno chcesz się wylogować?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Nowy URI" }, + "addDomain": { + "message": "Dodaj domenę", + "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": { "message": "Element został dodany" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Poproś o dodanie danych logowania" }, + "vaultSaveOptionsTitle": { + "message": "Zapisz do ustawień sejfu" + }, "addLoginNotificationDesc": { "message": "\"Dodaj powiadomienia logowania\" automatycznie wyświetla monit o zapisanie nowych danych logowania do sejfu przy każdym pierwszym logowaniu." }, "addLoginNotificationDescAlt": { "message": "Poproś o dodanie elementu, jeśli nie zostanie znaleziony w Twoim sejfie. Dotyczy wszystkich zalogowanych kont." }, + "showCardsInVaultView": { + "message": "Pokaż karty jako sugestie autouzupełniania w widoku sejfu" + }, "showCardsCurrentTab": { "message": "Pokaż karty na stronie głównej" }, "showCardsCurrentTabDesc": { "message": "Pokaż elementy karty na stronie głównej, aby ułatwić autouzupełnianie." }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" + }, "showIdentitiesCurrentTab": { "message": "Pokaż tożsamości na stronie głównej" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Domyślne wykrywanie dopasowania", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Wybierz domyślny sposób wykrywania dopasowania adresów dla czynności takich jak autouzupełnianie." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB miejsca na zaszyfrowane załączniki." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Własnościowe opcje logowania dwuetapowego, takie jak YubiKey i Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Konto Premium możesz zakupić na stronie sejfu bitwarden.com. Czy chcesz otworzyć tę stronę?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Posiadasz konto Premium!" }, "premiumCurrentMemberThanks": { "message": "Dziękujemy za wspieranie Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "Wszystko to jedynie za $PRICE$ /rok!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Odświeżanie zostało zakończone" }, @@ -1179,9 +1342,18 @@ }, "showAutoFillMenuOnFormFields": { "message": "Pokaż menu autouzupełniania na polach formularza", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Sugestie autouzupełniania" + }, + "showInlineMenuLabel": { + "message": "Pokaż sugestie autouzupełniania na polach formularza" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Wyświetlaj sugestie kiedy ikona jest zaznaczona" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Dotyczy wszystkich zalogowanych kont." }, "turnOffBrowserBuiltInPasswordManagerSettings": { @@ -1202,15 +1374,34 @@ "message": "Gdy wybrano ikonę autouzupełniania", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Włącz autouzupełnianie po załadowaniu strony" + }, "enableAutoFillOnPageLoad": { "message": "Włącz autouzupełnianie po załadowaniu strony" }, "enableAutoFillOnPageLoadDesc": { "message": "Jeśli zostanie wykryty formularz logowania, automatycznie uzupełnij dane logowania po załadowaniu strony." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Ostrzeżenie:$CLOSETAG$ Niebezpieczne witryny są w stanie wykorzystać autouzupełnianie przy wczytywaniu strony.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Zaatakowane lub niezaufane witryny internetowe mogą wykorzystać funkcję autouzupełniania podczas wczytywania strony, aby wyrządzić szkody." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Dowiedz się więcej o ryzyku" + }, "learnMoreAboutAutofill": { "message": "Dowiedz się więcej o autouzupełnianiu" }, @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Otwórz sejf na pasku bocznym" }, - "commandAutofillDesc": { - "message": "Autouzupełnianie korzysta z ostatnio używanych danych logowania na tej stronie." + "commandAutofillLoginDesc": { + "message": "Autouzupełnianie korzysta z ostatnio używanych danych logowania na tej stronie" + }, + "commandAutofillCardDesc": { + "message": "Autouzupełnianie korzysta z ostatnio używanych danych karty na tej stronie" + }, + "commandAutofillIdentityDesc": { + "message": "Autouzupełnianie korzysta z ostatnio używanej tożsamości na tej stronie" }, "commandGeneratePasswordDesc": { "message": "Wygeneruj nowe losowe hasło i skopiuj je do schowka." @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Wartość logiczna" }, + "cfTypeCheckbox": { + "message": "Pole wyboru" + }, "cfTypeLinked": { "message": "Powiązane pole", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "Zobacz $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Historia hasła" }, @@ -1533,6 +1742,10 @@ "message": "Domena podstawowa", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Domena podstawowa (rekomendowana)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Nazwa domeny", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Wykrywanie dopasowania", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Domyślne wykrywanie dopasowania", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Zmień opcje" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Brak haseł." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Usuń" }, @@ -1677,6 +1899,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" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Nowe hasło główne nie spełnia wymaganych zasad." }, - "receiveMarketingEmails": { - "message": "Otrzymuj e-maile od Bitwarden z ogłoszeniami, poradami i badaniami." + "receiveMarketingEmailsV2": { + "message": "Uzyskaj poradę, ogłoszenia i możliwości badawcze od Bitwarden w swojej skrzynce odbiorczej." }, "unsubscribe": { "message": "Anuluj subskrypcję" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Odblokuj tego użytkownika w aplikacji desktopowej i spróbuj ponownie." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Dane biometryczne są błędne" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "Polityka organizacji zablokowała importowanie elementów do Twojego sejfu." }, + "domainsTitle": { + "message": "Domeny", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Wykluczone domeny" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Aplikacja Bitwarden nie będzie proponować zapisywania danych logowania dla tych domen dla wszystkich zalogowanych kont. Musisz odświeżyć stronę, aby zastosowywać zmiany." }, + "websiteItemLabel": { + "message": "Strona internetowa $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ nie jest prawidłową nazwą domeny", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Zmiany w wykluczonych domenach zapisane" + }, "send": { "message": "Wyślij", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Chroniona hasłem" }, + "copyLink": { + "message": "Kopiuj link" + }, "copySendLink": { "message": "Kopiuj link wysyłki", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Wysyłka została utworzona", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Wysyłka została zapisana", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Weryfikacja adresu e-mail jest wymagana" }, + "emailVerifiedV2": { + "message": "E-mail zweryfikowany" + }, "emailVerificationRequiredDesc": { "message": "Musisz zweryfikować adres e-mail, aby korzystać z tej funkcji. Adres możesz zweryfikować w sejfie internetowym." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatyczne rejestrowanie użytkowników" }, @@ -2629,13 +2929,22 @@ "autofillSettings": { "message": "Ustawienia autouzupełniania" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Skrót autouzupełniania" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Zmień skrót" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Zarządzaj skrótami" + }, "autofillShortcut": { "message": "Skrót klawiaturowy autouzupełniania" }, - "autofillShortcutNotSet": { + "autofillLoginShortcutNotSet": { "message": "Skrót autouzupełniania nie jest ustawiony. Zmień to w ustawieniach przeglądarki." }, - "autofillShortcutText": { + "autofillLoginShortcutText": { "message": "Skrót autouzupełniania to: $COMMAND$. Zmień to w ustawieniach przeglądarki.", "placeholders": { "command": { @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Zaufano urządzeniu" }, + "sendsNoItemsTitle": { + "message": "Brak aktywnych wysyłek", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Użyj wysyłki, aby bezpiecznie dzielić się zaszyfrowanymi informacjami ze wszystkimi.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Dane wejściowe są wymagane." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 pole wymaga Twojej uwagi." + }, + "multipleFieldsNeedAttention": { + "message": "Pola wymagające Twojej uwagi: $COUNT$.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Wybierz --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Elementy z pytaniem o hasło głównege nie mogą być automatycznie wypełniane przy wczytywaniu strony. Automatyczne wypełnianie po wczytywania strony zostało wyłączone.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "Automatyczne wypełnianie przy wczytywaniu strony zostało ustawione, aby używać ustawień domyślnych.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Wyłącz prośbę o podanie hasła głównego, aby edytować to pole", @@ -2911,10 +3240,18 @@ "message": "Odblokuj swoje konto, aby wyświetlić pasujące elementy", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Odblokuj swoje konto, aby zobaczyć sugestie autouzupełniania", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Odblokuj konto", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Odblokuj swoje konto, otwiera się w nowym oknie", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Wypełnij dane logowania dla", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Dodaj nowy element do sejfu", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "Nowe dane logowania", + "description": "Button text to display within inline menu when there are no matching items on a login field" + }, + "addNewLoginItemAria": { + "message": "Dodaj nowe dane logowania do sejfu, otwiera się w nowym oknie", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "Nowa karta", + "description": "Button text to display within inline menu when there are no matching items on a credit card field" + }, + "addNewCardItemAria": { + "message": "Dodaj nową kartę do sejfu, otwiera się w nowym oknie", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "Nowa tożsamość", + "description": "Button text to display within inline menu when there are no matching items on an identity field" + }, + "addNewIdentityItemAria": { + "message": "Dodaj nową tożsamość do sejfu, otwiera się w nowym oknie", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Dostępne menu autouzupełniania Bitwarden. Naciśnij przycisk strzałki w dół, aby wybrać.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Wystąpił błąd podczas połączenia z usługą Duo. Aby uzyskać pomoc, użyj innej metody dwustopniowego logowania lub skontaktuj się z Duo." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Uruchom DUO i wykonaj kroki, aby zakończyć logowanie." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Hasło do pliku jest nieprawidłowe. Użyj hasła które podano przy tworzeniu pliku eksportu." }, - "importDestination": { - "message": "Miejsce docelowe importu" + "destination": { + "message": "Miejsce docelowe" }, "learnAboutImportOptions": { "message": "Dowiedz się więcej o opcjach importu" @@ -3122,8 +3486,8 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Weryfikacja jest wymagana przez stronę inicjującą. Ta funkcja nie jest jeszcze zaimplementowana dla kont bez hasła głównego." }, - "logInWithPasskey": { - "message": "Zaloguj się za pomocą passkey?" + "logInWithPasskeyQuestion": { + "message": "Log in with passkey?" }, "passkeyAlreadyExists": { "message": "Passkey już istnieje dla tej aplikacji." @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "Nie masz pasujących danych logowania do tej witryny." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Potwierdź" }, @@ -3143,8 +3510,11 @@ "savePasskeyNewLogin": { "message": "Zapisz passkey jako nowe dane logowania" }, - "choosePasskey": { - "message": "Wybierz dane logowania do których przypisać passkey" + "chooseCipherForPasskeySave": { + "message": "Choose a login to save this passkey to" + }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" }, "passkeyItem": { "message": "Element Passkey" @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,6 +3659,30 @@ "message": "Popularne formaty", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Kontynuować do ustawień przeglądarki?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Kontynuować do centrum pomocy?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" + }, + "confirmContinueToHelpCenterPasswordManagementContent": { + "message": "Zmień ustawienia autouzupełniania przeglądarki i zarządzania hasłami.", + "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": "Możesz przeglądać i ustawiać skróty klawiaturowe rozszerzeń w ustawieniach przeglądarki.", + "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": "Zmień ustawienia autouzupełniania przeglądarki i zarządzania hasłami.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" + }, + "confirmContinueToBrowserKeyboardShortcutSettingsContent": { + "message": "Możesz przeglądać i ustawiać skróty klawiaturowe rozszerzeń w ustawieniach przeglądarki.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Czy Bitwarden ma być domyślnym menadżerem haseł?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" @@ -3317,10 +3711,18 @@ "message": "Pomyślnie zapisano dane logowania!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Pomyślnie zaktualizowano dane logowania!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Błąd podczas zapisywania danych logowania. Sprawdź konsolę, aby uzyskać szczegóły.", "description": "Notification message for when saving credentials has failed." @@ -3334,20 +3736,6 @@ "passkeyRemoved": { "message": "Passkey został usunięty" }, - "unassignedItemsBannerNotice": { - "message": "Uwaga: Nieprzypisane elementy organizacji nie są już widoczne w widoku Wszystkie sejfy i są teraz dostępne tylko przez Konsolę Administracyjną." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Uwaga: 16 maja 2024 r. nieprzypisana elementy w organizacji nie będą już widoczne w widoku Wszystkie sejfy i będą dostępne tylko przez Konsolę Administracyjną." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Przypisz te elementy do kolekcji z", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ", aby były widoczne.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Sugestie autouzupełnienia" }, @@ -3415,7 +3803,7 @@ }, "autofillTitle": { "message": "Autouzupełnij - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "Brak wartości do skopiowania" }, - "assignCollections": { - "message": "Przypisz kolekcje" + "assignToCollections": { + "message": "Przypisz do kolekcji" }, "copyEmail": { "message": "Skopiuj e-mail" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Nie można uzyskać dostępu do elementów w wyłączonych organizacjach. Skontaktuj się z właścicielem organizacji, aby uzyskać pomoc." }, + "additionalInformation": { + "message": "Dodatkowe informacje" + }, + "itemHistory": { + "message": "Historia elementu" + }, + "lastEdited": { + "message": "Ostatnio edytowany" + }, + "ownerYou": { + "message": "Właściciel: Ty" + }, + "linked": { + "message": "Powiązane" + }, + "copySuccessful": { + "message": "Kopiowanie zakończone sukcesem" + }, "upload": { "message": "Wyślij" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filtry" + }, + "personalDetails": { + "message": "Dane osobowe" + }, + "identification": { + "message": "Tożsamość" + }, + "contactInfo": { + "message": "Daje kontaktowe" + }, + "downloadAttachment": { + "message": "Pobierz - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "numer karty kończy się", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Dane logowania" + }, + "authenticatorKey": { + "message": "Klucz uwierzytelniający" + }, + "autofillOptions": { + "message": "Opcje autouzupełniania" + }, + "websiteUri": { + "message": "Strona internetowa (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": "Strona dodana" + }, + "addWebsite": { + "message": "Dodaj stronę internetową" + }, + "deleteWebsite": { + "message": "Usuń stronę internetową" + }, + "defaultLabel": { + "message": "Domyślnie ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Włącz autouzupełnianie po załadowaniu strony?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Szczegóły karty" + }, + "cardBrandDetails": { + "message": "Szczegóły $BRAND$", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Dodaj konto" + }, + "loading": { + "message": "Wczytywanie" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Tylko członkowie organizacji z dostępem do tych kolekcji będą mogli zobaczyć ten element." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Tylko członkowie organizacji z dostępem do tych kolekcji będą mogli zobaczyć te elementy." + }, + "bulkCollectionAssignmentWarning": { + "message": "Wybrałeś $TOTAL_COUNT$ elementów. Nie możesz zaktualizować $READONLY_COUNT$ elementów, ponieważ nie masz uprawnień do edycji.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Dodaj pole" + }, + "add": { + "message": "Dodaj" + }, + "fieldType": { + "message": "Typ pola" + }, + "fieldLabel": { + "message": "Etykieta pola" + }, + "textHelpText": { + "message": "Użyj pól tekstowych dla danych takich jak pytania bezpieczeństwa" + }, + "hiddenHelpText": { + "message": "Użyj ukrytych pól dla danych poufnych, takich jak hasło" + }, + "checkBoxHelpText": { + "message": "Użyj pól wyboru, jeśli chcesz automatycznie wypełnić pole wyboru formularza, np. zapamiętaj e-mail" + }, + "linkedHelpText": { + "message": "Użyj powiązanego pola, gdy masz problemy z autouzupełnianiem na konkretnej stronie internetowej." + }, + "linkedLabelHelpText": { + "message": "Wprowadź atrybut z HTML'a: id, name, aria-label lub placeholder." + }, + "editField": { + "message": "Edytuj pole" + }, + "editFieldLabel": { + "message": "Edytuj $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Usuń $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "Dodano $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Zmień kolejność $LABEL$. Użyj klawiszy że strzałkami aby przenieść element w górę lub w dół.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ przeniósł się w górę, pozycja $INDEX$ z $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Wybierz kolekcje do przypisania" + }, + "personalItemTransferWarningSingular": { + "message": "1 element zostanie trwale przeniesiony do wybranej organizacji. Nie będziesz już posiadać tego elementu." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ elementów zostanie trwale przeniesionych do wybranej organizacji. Nie będziesz już posiadać tych elementów.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 przedmiot zostanie trwale przeniesiony do $ORG$. Nie będziesz już posiadać tego przedmiotu.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ elementy zostaną trwale przeniesione do $ORG$. Nie będziesz już posiadać tych elementów.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Pomyślnie przypisano kolekcje" + }, + "nothingSelected": { + "message": "Nie zaznaczyłeś żadnych elementów." + }, + "movedItemsToOrg": { + "message": "Zaznaczone elementy zostały przeniesione do $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Elementy przeniesione do $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Element przeniesiony do $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ przeniósł się w dół, pozycja $INDEX$ z $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Lokalizacja elementu" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden ma nowy wygląd!" + }, + "bitwardenNewLookDesc": { + "message": "Auto wypełnianie i szukanie na zakładce sejfu jest teraz prostsze i bardziej intuicyjne. Rozejrzyj się tam!" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 78367670a46..a88aae11814 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Inicie a sessão ou crie uma nova conta para acessar seu cofre seguro." }, + "inviteAccepted": { + "message": "Convite aceito" + }, "createAccount": { "message": "Criar Conta" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Termine de criar a sua conta definindo uma senha" }, - "login": { - "message": "Iniciar Sessão" - }, "enterpriseSingleSignOn": { "message": "Iniciar Sessão Empresarial Única" }, @@ -50,7 +50,7 @@ "message": "Uma dica de senha mestra pode ajudá-lo(a) a lembrá-lo(a) caso você esqueça." }, "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 de senha pode ser enviada ao seu e-mail. $CURRENT$/$MAXIMUM$ caracteres máximos.", "placeholders": { "current": { "content": "$1", @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Dica de Senha Mestra (opcional)" }, + "joinOrganization": { + "message": "Juntar-se à organização" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Termine de juntar-se nessa organização definindo uma senha mestra." + }, "tab": { "message": "Aba" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Copiar Código de Segurança" }, + "copyName": { + "message": "Copiar nome" + }, + "copyCompany": { + "message": "Copiar empresa" + }, + "copySSN": { + "message": "Cadastro de Pessoas Físicas" + }, + "copyPassportNumber": { + "message": "Copiar número do passaporte" + }, + "copyLicenseNumber": { + "message": "Copiar número da CNH" + }, "autoFill": { "message": "Autopreencher" }, @@ -186,7 +207,7 @@ "message": "Confirme a sua identidade para continuar." }, "changeMasterPassword": { - "message": "Alterar Senha Mestra" + "message": "Alterar senha mestra" }, "continueToWebApp": { "message": "Continuar no aplicativo web?" @@ -280,6 +301,24 @@ "editFolder": { "message": "Editar Pasta" }, + "newFolder": { + "message": "Nova pasta" + }, + "folderName": { + "message": "Nome da pasta" + }, + "folderHintText": { + "message": "Aninhe uma pasta adicionando o nome da pasta pai seguido de um \"/\". Exemplo: Social/Fóruns" + }, + "noFoldersAdded": { + "message": "Nenhuma pasta adicionada" + }, + "createFoldersToOrganize": { + "message": "Crie pastas para organizar os itens do seu cofre" + }, + "deleteFolderPermanently": { + "message": "Você tem certeza que deseja excluir esta pasta permanentemente?" + }, "deleteFolder": { "message": "Excluir Pasta" }, @@ -345,16 +384,56 @@ "message": "Tamanho mínimo da senha" }, "uppercase": { - "message": "Maiúsculas (A-Z)" + "message": "Maiúsculas (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Minúsculas (a-z)" + "message": "Minúsculas (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Números (0-9)" + "message": "Números (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Caracteres especiais (!@#$%^&*)" + "message": "Caracteres especiais (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Incluir", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Incluir caracteres maiúsculos", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Incluir caracteres minúsculos", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Incluir números", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0 – 9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Incluir caracteres especiais", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Número de Palavras" @@ -376,7 +455,12 @@ "message": "Especiais Mínimos" }, "avoidAmbChar": { - "message": "Evitar Caracteres Ambíguos" + "message": "Evitar Caracteres Ambíguos", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Evitar Caracteres Ambíguos", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Pesquisar no Cofre" @@ -556,6 +640,18 @@ "security": { "message": "Segurança" }, + "confirmMasterPassword": { + "message": "Confirme a senha mestra" + }, + "masterPassword": { + "message": "Senha mestra" + }, + "masterPassImportant": { + "message": "Sua senha mestra não pode ser recuperada se você a esquecer!" + }, + "masterPassHintLabel": { + "message": "Dica da senha mestra" + }, "errorOccurred": { "message": "Ocorreu um erro" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "A sua nova conta foi criada! Agora você pode iniciar a sessão." }, + "newAccountCreated2": { + "message": "Sua nova conta foi criada!" + }, + "youHaveBeenLoggedIn": { + "message": "Você está conectado!" + }, "youSuccessfullyLoggedIn": { "message": "Você logou na sua conta com sucesso" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "O código de verificação é necessário." }, + "webauthnCancelOrTimeout": { + "message": "A autenticação foi cancelada ou demorou muito. Por favor tente novamente." + }, "invalidVerificationCode": { "message": "Código de verificação inválido" }, @@ -613,7 +718,7 @@ } }, "autofillError": { - "message": "Não é possível autopreencher o item selecionado nesta página. Em alternativa, copie e cole a informação." + "message": "Não é possível auto-preencher o item selecionado nesta página. Em alternativa, copie e cole a informação." }, "totpCaptureError": { "message": "Não foi possível escanear o código QR a partir da página atual" @@ -624,6 +729,18 @@ "totpCapture": { "message": "Escaneie o código QR do autenticador na página atual" }, + "totpHelperTitle": { + "message": "Tornar a verificação em duas etapas fácil" + }, + "totpHelper": { + "message": "O Bitwarden pode armazenar e preencher códigos de verificação de duas etapas. Copie e cole a chave neste campo." + }, + "totpHelperWithCapture": { + "message": "O Bitwarden pode armazenar e preencher códigos de verificação de duas etapas. Selecione o ícone de câmera para tirar uma captura da tela do código QR de autenticador deste site, ou copie e cole a chave neste campo." + }, + "learnMoreAboutAuthenticators": { + "message": "Saiba mais sobre os autenticadores" + }, "copyTOTP": { "message": "Copiar chave de Autenticação (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "A sua sessão expirou." }, + "logIn": { + "message": "Fazer login" + }, + "restartRegistration": { + "message": "Reiniciar registro" + }, + "expiredLink": { + "message": "Link expirado" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Por favor, reinicie o registro ou tente fazer login." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Você pode já ter uma conta" + }, "logOutConfirmation": { "message": "Você tem certeza que deseja sair?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Novo URI" }, + "addDomain": { + "message": "Adicionar domínio", + "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": { "message": "Item adicionado" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Peça para adicionar login" }, + "vaultSaveOptionsTitle": { + "message": "Salvar nas opções do cofre" + }, "addLoginNotificationDesc": { "message": "A \"Notificação de Adicionar Login\" pede para salvar automaticamente novas logins para o seu cofre quando você inicia uma sessão em um site pela primeira vez." }, "addLoginNotificationDescAlt": { "message": "Pedir para adicionar um item se um não for encontrado no seu cofre. Aplica-se a todas as contas logadas." }, + "showCardsInVaultView": { + "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." }, "showCardsCurrentTabDesc": { "message": "Exibir itens de cartão em páginas com abas para simplificar o preenchimento automático" }, + "showIdentitiesInVaultView": { + "message": "Mostrar identifica como sugestões de preenchimento automático na exibição do Cofre" + }, "showIdentitiesCurrentTab": { "message": "Exibir Identidades na Aba Atual" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Detecção de Correspondência de URI Padrão", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Escolha a maneira padrão pela qual a detecção de correspondência de URI é manipulada para logins ao executar ações como preenchimento automático." @@ -855,7 +1000,7 @@ "message": "Esta senha será usada para exportar e importar este arquivo" }, "accountRestrictedOptionDescription": { - "message": "Use sua chave criptográfica da conta, derivada do nome de usuário e Senha Mestra da sua conta, para criptografar a exportação e restringir importação para apenas a conta atual do Bitwarden." + "message": "Use sua chave criptográfica da conta, derivada do nome de usuário e senha mestra da sua conta, para criptografar a exportação e restringir importação para apenas a conta atual do Bitwarden." }, "passwordProtectedOptionDescription": { "message": "Defina uma senha de arquivo para criptografar a exportação e importá-la para qualquer conta do Bitwarden usando a senha para descriptografia." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB de armazenamento de arquivos encriptados." }, + "premiumSignUpEmergency": { + "message": "Acesso de emergência." + }, "premiumSignUpTwoStepOptions": { "message": "Opções de login em duas etapas como YubiKey e Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Você pode comprar a assinatura premium no cofre web em bitwarden.com. Você deseja visitar o site agora?" }, + "premiumPurchaseAlertV2": { + "message": "Você pode comprar Premium nas configurações de sua conta no aplicativo web do Bitwarden." + }, "premiumCurrentMember": { "message": "Você é um membro premium!" }, "premiumCurrentMemberThanks": { "message": "Obrigado por apoiar o Bitwarden." }, + "premiumFeatures": { + "message": "Atualize para a versão Premium e receba:" + }, "premiumPrice": { "message": "Tudo por apenas %price% /ano!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "Tudo por apenas $PRICE$ por ano!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Atualização completa" }, @@ -1106,17 +1269,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": { @@ -1133,7 +1296,7 @@ "message": "E-mail" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Digite o código enviado para seu e-mail." }, "selfHostedEnvironment": { "message": "Ambiente Auto-hospedado" @@ -1179,9 +1342,18 @@ }, "showAutoFillMenuOnFormFields": { "message": "Exibir o menu de preenchimento automático nos campos do formulário", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Sugestões de preenchimento automático" + }, + "showInlineMenuLabel": { + "message": "Mostrar sugestões de preenchimento automático nos campos de formulários" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Exibir sugestões quando o ícone for selecionado" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Aplica-se a todas as contas conectadas." }, "turnOffBrowserBuiltInPasswordManagerSettings": { @@ -1202,15 +1374,34 @@ "message": "Quando o ícone de preenchimento automático for selecionado", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Preenchimento automático ao carregar a página" + }, "enableAutoFillOnPageLoad": { - "message": "Ativar o Autopreenchimento ao Carregar a Página" + "message": "Auto-preencher ao carregar a página" }, "enableAutoFillOnPageLoadDesc": { "message": "Se um formulário de login for detectado, realizar automaticamente um auto-preenchimento quando a página web carregar." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Aviso:$CLOSETAG$ Comprometido ou sites não confiáveis podem explorar o autopreenchimento ao carregar a página.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Sites comprometidos ou não confiáveis podem tomar vantagem do autopreenchimento ao carregar a página." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Saiba mais sobre riscos" + }, "learnMoreAboutAutofill": { "message": "Saiba mais sobre preenchimento automático" }, @@ -1227,10 +1418,10 @@ "message": "Usar configuração padrão" }, "autoFillOnPageLoadYes": { - "message": "Autopreencher ao carregar a página" + "message": "Auto-preencher ao carregar a página" }, "autoFillOnPageLoadNo": { - "message": "Não autopreencher ao carregar a página" + "message": "Não auto-preencher ao carregar a página" }, "commandOpenPopup": { "message": "Abrir pop-up do cofre" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Abrir cofre na barra lateral" }, - "commandAutofillDesc": { - "message": "Autopreencher o último login utilizado para o site atual." + "commandAutofillLoginDesc": { + "message": "Preencher automaticamente o último login utilizado para o site atual" + }, + "commandAutofillCardDesc": { + "message": "Preenchimento automático do último cartão utilizado para o site atual" + }, + "commandAutofillIdentityDesc": { + "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." @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Booleano" }, + "cfTypeCheckbox": { + "message": "Caixa de seleção" + }, "cfTypeLinked": { "message": "Vinculado", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "Visualizar $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Histórico de Senha" }, @@ -1508,7 +1717,7 @@ "message": "Credenciais" }, "secureNotes": { - "message": "Notas Seguras" + "message": "Notas seguras" }, "clear": { "message": "Limpar", @@ -1533,6 +1742,10 @@ "message": "Domínio de base", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Domínio de base (recomendado)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Nome do domínio", "description": "Domain name. Ex. website.com" @@ -1552,12 +1765,12 @@ "description": "A programming term, also known as 'RegEx'." }, "matchDetection": { - "message": "Detecção de Correspondência", - "description": "URI match detection for auto-fill." + "message": "Detecção de correspondência", + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Detecção de correspondência padrão", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Alternar Opções" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Não existem senhas para listar." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Remover" }, @@ -1629,7 +1851,7 @@ "description": "ex. A weak password. Scale: Weak -> Good -> Strong" }, "weakMasterPassword": { - "message": "Senha Mestra Fraca" + "message": "Senha mestra fraca" }, "weakMasterPasswordDesc": { "message": "A senha mestra que você selecionou está fraca. Você deve usar uma senha mestra forte (ou uma frase-passe) para proteger a sua conta Bitwarden adequadamente. Tem certeza que deseja usar esta senha mestra?" @@ -1677,6 +1899,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" }, @@ -1746,7 +1991,7 @@ } }, "setMasterPassword": { - "message": "Definir Senha Mestra" + "message": "Definir senha mestra" }, "currentMasterPass": { "message": "Senha mestra atual" @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "A sua nova senha mestra não cumpre aos requisitos da política." }, - "receiveMarketingEmails": { - "message": "Obtenha e-mails do Bitwarden para anúncios, conselhos e oportunidades de pesquisa." + "receiveMarketingEmailsV2": { + "message": "Obtenha conselhos, novidades, e oportunidades de pesquisa do Bitwarden em sua caixa de entrada." }, "unsubscribe": { "message": "Cancelar subscrição" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Por favor, desbloqueie esse usuário no aplicativo da área de trabalho e tente novamente." }, + "biometricsNotAvailableTitle": { + "message": "Desbloqueio biométrico indisponível" + }, + "biometricsNotAvailableDesc": { + "message": "O desbloqueio biométrico está indisponível no momento. Tente novamente mais tarde." + }, "biometricsFailedTitle": { "message": "Biometria falhou" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "A política da organização bloqueou a importação de itens para o seu cofre." }, + "domainsTitle": { + "message": "Domínios", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Domínios Excluídos" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "O Bitwarden não irá pedir para salvar os detalhes de credencial para estes domínios. Você deve atualizar a página para que as alterações entrem em vigor." }, + "websiteItemLabel": { + "message": "Site $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ não é um domínio válido", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Mudanças de domínios excluídos salvas" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Protegido por senha" }, + "copyLink": { + "message": "Copiar link" + }, "copySendLink": { "message": "Copiar link do Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send Criado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Envio criado com sucesso!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "O envio estará disponível para qualquer pessoa com o link para os próximos $DAYS$ dias.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Enviar link copiado", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send Editado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,21 +2458,27 @@ "emailVerificationRequired": { "message": "Verificação de E-mail Necessária" }, + "emailVerifiedV2": { + "message": "E-mail verificado" + }, "emailVerificationRequiredDesc": { "message": "Você precisa verificar o seu e-mail para usar este recurso. Você pode verificar seu e-mail no cofre web." }, "updatedMasterPassword": { - "message": "Senha Mestra Atualizada" + "message": "Senha mestra atualizada" }, "updateMasterPassword": { - "message": "Atualizar Senha Mestra" + "message": "Atualizar senha mestra" }, "updateMasterPasswordWarning": { - "message": "Sua Senha Mestra foi alterada recentemente por um administrador de sua organização. Para acessar o cofre, você precisa atualizá-la 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." + "message": "Sua senha mestra foi alterada recentemente por um administrador de sua organização. Para acessar o cofre, você precisa atualizá-la 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." }, "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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Inscrição Automática" }, @@ -2277,7 +2577,7 @@ "message": "Sair da Organização" }, "removeMasterPassword": { - "message": "Remover Senha Mestra" + "message": "Remover senha mestra" }, "removedMasterPassword": { "message": "Senha mestra removida." @@ -2576,13 +2876,13 @@ "message": "Login iniciado" }, "exposedMasterPassword": { - "message": "Senha Mestra comprometida" + "message": "Senha mestra comprometida" }, "exposedMasterPasswordDesc": { "message": "A senha foi encontrada em um vazamento de dados. Use uma senha única para proteger sua conta. Tem certeza de que deseja usar uma senha já exposta?" }, "weakAndExposedMasterPassword": { - "message": "Senha Mestra fraca e comprometida" + "message": "Senha mestra fraca e comprometida" }, "weakAndBreachedMasterPasswordDesc": { "message": "Senha fraca identificada e encontrada em um vazamento de dados. Use uma senha forte e única para proteger a sua conta. Tem certeza de que deseja usar essa senha?" @@ -2594,7 +2894,7 @@ "message": "Importante:" }, "masterPasswordHint": { - "message": "Sua Senha Mestra não pode ser recuperada se você a esquecer!" + "message": "Sua senha mestra não pode ser recuperada se você a esquecer!" }, "characterMinimum": { "message": "$LENGTH$ caracteres mínimos", @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Configurações de autopreenchimento" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Alterar atalho" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Gerenciar atalhos" + }, "autofillShortcut": { "message": "Atalho para autopreenchimento" }, - "autofillShortcutNotSet": { - "message": "O atalho de preenchimento automático não está definido. Altere-o nas configurações do navegador." + "autofillLoginShortcutNotSet": { + "message": "O atalho de acesso ao preenchimento automático não está definido. Altere isso nas configurações do navegador." }, - "autofillShortcutText": { - "message": "O atalho de preenchimento automático é: $COMMAND$. Altere-o nas configurações do navegador.", + "autofillLoginShortcutText": { + "message": "O atalho de login de preenchimento automático é $COMMAND$. Gerencie todos os atalhos nas configurações do navegador.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Dispositivo confiável" }, + "sendsNoItemsTitle": { + "message": "Nenhum Send ativo", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use o Send para compartilhar informação criptografa com qualquer um.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Entrada necessária." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 campo precisa de sua atenção." + }, + "multipleFieldsNeedAttention": { + "message": "Campos $COUNT$ precisam de sua atenção.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Selecione --" }, @@ -2879,18 +3208,18 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Os itens com confirmação de senha mestra não podem ser auto-preenchidos ao carregar a página. O carregamento da página será desativado.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "Definir preenchimento automático ao carregar página para usar a configuração padrão.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { - "message": "Desative o prompt de senha mestra para editar este campo", + "message": "Desative a re-solicitação de senha mestra para editar este campo", "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "Ativar/desativar navegação lateral" }, "skipToContent": { "message": "Ir para o conteúdo" @@ -2911,10 +3240,18 @@ "message": "Desbloqueie sua conta para ver os logins correspondentes", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "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": { "message": "Desbloquear conta", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Desbloqueie sua conta, abra em uma nova janela", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Preencha as credenciais para", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Adicionar novo item do cofre", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "Novo login", + "description": "Button text to display within inline menu when there are no matching items on a login field" + }, + "addNewLoginItemAria": { + "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": "Novo cartão", + "description": "Button text to display within inline menu when there are no matching items on a credit card field" + }, + "addNewCardItemAria": { + "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": "Nova identidade", + "description": "Button text to display within inline menu when there are no matching items on an identity field" + }, + "addNewIdentityItemAria": { + "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": { "message": "Menu de autopreenchimento do Bitwarden disponível. Pressione a tecla de seta para baixo para selecionar.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "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." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Senha do arquivo inválida, por favor informe a senha utilizada quando criou o arquivo de exportação." }, - "importDestination": { - "message": "Destino da Importação" + "destination": { + "message": "Destino" }, "learnAboutImportOptions": { "message": "Saiba mais sobre suas opções de importação" @@ -3108,7 +3472,7 @@ "message": "Confirmar senha do arquivo" }, "exportSuccess": { - "message": "Vault data exported" + "message": "Dados do cofre exportados" }, "typePasskey": { "message": "Chave de acesso" @@ -3122,8 +3486,8 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verificação requerida pelo site que a iniciou. Esse recurso ainda não está implementado para contas sem senha mestra." }, - "logInWithPasskey": { - "message": "Fazer login com chave de acesso?" + "logInWithPasskeyQuestion": { + "message": "Fazer ‘login’ com chave de acesso?" }, "passkeyAlreadyExists": { "message": "Uma chave de acesso já existe para este aplicativo." @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "Você não tem um login correspondente para este site." }, + "noMatchingLoginsForSite": { + "message": "Sem credenciais correspondentes para este site" + }, "confirm": { "message": "Confirmar" }, @@ -3143,8 +3510,11 @@ "savePasskeyNewLogin": { "message": "Salvar chave de acesso como um novo login" }, - "choosePasskey": { - "message": "Escolha um login para salvar esta chave de acesso" + "chooseCipherForPasskeySave": { + "message": "Escolha um ‘login’ para salvar com essa chave de acesso" + }, + "chooseCipherForPasskeyAuth": { + "message": "Escolha uma senha para iniciar sessão" }, "passkeyItem": { "message": "Item de chave de acesso" @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,6 +3659,30 @@ "message": "Formatos comuns", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "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": "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": "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": "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": "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": "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": { "message": "Tornar o Bitwarden seu gerenciador de senhas padrão?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" @@ -3317,10 +3711,18 @@ "message": "Credenciais salvas com sucesso!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Senha salva!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credenciais atualizadas com sucesso!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Senha atualizada!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Erro ao salvar credenciais. Verifique o console para detalhes.", "description": "Notification message for when saving credentials has failed." @@ -3334,20 +3736,6 @@ "passkeyRemoved": { "message": "Chave de acesso removida" }, - "unassignedItemsBannerNotice": { - "message": "Aviso: Itens da organização não atribuídos não estão mais visíveis na visualização Todos os Cofres e só são acessíveis por meio do painel de administração." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Aviso: Em 16 de maio, 2024, itens da organização não serão mais visíveis na visualização Todos os Cofres e só serão acessíveis por meio do painel de administração." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Atribua estes itens a uma coleção da", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "para torná-los visíveis.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Sugestões de autopreenchimento" }, @@ -3415,7 +3803,7 @@ }, "autofillTitle": { "message": "Auto-preenchimento - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "Não há valores para copiar" }, - "assignCollections": { - "message": "Aplicar coleção" + "assignToCollections": { + "message": "Atribuir à coleções" }, "copyEmail": { "message": "Copiar e-mail" @@ -3493,13 +3881,13 @@ "message": "Itens sem pasta" }, "itemDetails": { - "message": "Item details" + "message": "Detalhes dos item" }, "itemName": { - "message": "Item name" + "message": "Nome do item" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Você não pode remover coleções com permissões de Somente leitura: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,26 +3899,44 @@ "message": "A organização está desativada" }, "owner": { - "message": "Owner" + "message": "Proprietário" }, "selfOwnershipLabel": { - "message": "You", + "message": "Você", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Itens em organizações desativadas não podem ser acessados. Entre em contato com o proprietário da sua organização para obter assistência." }, + "additionalInformation": { + "message": "Informação adicional" + }, + "itemHistory": { + "message": "Histórico do item" + }, + "lastEdited": { + "message": "Última edição" + }, + "ownerYou": { + "message": "Proprietário: Você" + }, + "linked": { + "message": "Vinculado" + }, + "copySuccessful": { + "message": "Copiado com Sucesso" + }, "upload": { - "message": "Upload" + "message": "Fazer upload" }, "addAttachment": { - "message": "Add attachment" + "message": "Adicionar anexo" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "O tamanho máximo de arquivo é de 500 MB" }, "deleteAttachmentName": { - "message": "Delete attachment $NAME$", + "message": "Excluir anexo $NAME$", "placeholders": { "name": { "content": "$1", @@ -3539,7 +3945,7 @@ } }, "downloadAttachmentName": { - "message": "Download $NAME$", + "message": "Baixar $NAME$", "placeholders": { "name": { "content": "$1", @@ -3548,15 +3954,389 @@ } }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Are you sure you want to permanently delete this attachment?" + "message": "Tem certeza de que deseja excluir este anexo permanentemente?" }, "premium": { "message": "Premium" }, "freeOrgsCannotUseAttachments": { - "message": "Free organizations cannot use attachments" + "message": "Organizações gratuitas não podem usar anexos" }, "filters": { - "message": "Filters" + "message": "Filtros" + }, + "personalDetails": { + "message": "Detalhes pessoais" + }, + "identification": { + "message": "Identificação" + }, + "contactInfo": { + "message": "Informação de contato" + }, + "downloadAttachment": { + "message": "Baixar - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "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": { + "message": "Credenciais de login" + }, + "authenticatorKey": { + "message": "Chave do autenticador" + }, + "autofillOptions": { + "message": "Opções de autopreenchimento" + }, + "websiteUri": { + "message": "Site (URI)" + }, + "websiteUriCount": { + "message": "Site (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": "Site adicionado" + }, + "addWebsite": { + "message": "Adicionar site" + }, + "deleteWebsite": { + "message": "Excluir site" + }, + "defaultLabel": { + "message": "Padrão ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Exibir detecção de correspondência $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Ocultar detecção de correspondência $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Preenchimento automático ao carregar a página?" + }, + "cardExpiredTitle": { + "message": "Cartão expirado" + }, + "cardExpiredMessage": { + "message": "Se você o renovou, atualize as informações do cartão" + }, + "cardDetails": { + "message": "Detalhes do cartão" + }, + "cardBrandDetails": { + "message": "Detalhes $BRAND$", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Habilita animações" + }, + "addAccount": { + "message": "Adicionar conta" + }, + "loading": { + "message": "Carregando" + }, + "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": "Apenas membros da organização com acesso a essas coleções poderão ver o item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "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.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Adicionar campo" + }, + "add": { + "message": "Adicionar" + }, + "fieldType": { + "message": "Tipo do campo" + }, + "fieldLabel": { + "message": "Rótulo do campo" + }, + "textHelpText": { + "message": "Utilize campos de texto para dados como questões de segurança" + }, + "hiddenHelpText": { + "message": "Use campos ocultos para dados confidenciais como uma senha" + }, + "checkBoxHelpText": { + "message": "Use caixas de seleção se gostaria de preencher automaticamente a caixa de seleção de um formulário, como um e-mail de lembrança" + }, + "linkedHelpText": { + "message": "Use um campo vinculado quando estiver enfrentando problemas com o auto-preenchimento com um site específico." + }, + "linkedLabelHelpText": { + "message": "Digite o Id html do campo, nome, nome aria-label, ou espaço reservado." + }, + "editField": { + "message": "Editar campo" + }, + "editFieldLabel": { + "message": "Editar $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Excluir $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ adicionado", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reordene $LABEL$. Use a tecla de seta para mover o item para cima ou para baixo.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ se moveu para cima, posição $INDEX$ de $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Selecione as coleções para atribuir" + }, + "personalItemTransferWarningSingular": { + "message": "1 item será transferido permanentemente para a organização selecionada. Você não irá mais possuir este item." + }, + "personalItemsTransferWarningPlural": { + "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", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item será transferido permanentemente para $ORG$. Você não irá mais possuir este item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "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", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Coleções atribuídas com sucesso" + }, + "nothingSelected": { + "message": "Você selecionou nada." + }, + "movedItemsToOrg": { + "message": "Itens selecionados movidos para $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Itens movidos para $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item movido para $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ se moveu para baixo, posição $INDEX$ de $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Localização do Item" + }, + "fileSends": { + "message": "Arquivos enviados" + }, + "textSends": { + "message": "Texto enviado" + }, + "bitwardenNewLook": { + "message": "Bitwarden tem uma nova aparência!" + }, + "bitwardenNewLookDesc": { + "message": "É mais fácil e mais intuitivo do que nunca autopreenchimento e pesquise na guia Cofre. Dê uma olhada ao redor!" + }, + "accountActions": { + "message": "Ações da conta" + }, + "showNumberOfAutofillSuggestions": { + "message": "Mostrar o número de sugestões de preenchimento automático de login no ícone da extensão" + }, + "systemDefault": { + "message": "Padrão do sistema" + }, + "enterprisePolicyRequirementsApplied": { + "message": "Os requisitos de política empresarial foram aplicados nesta configuração" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Mostrar contagem de caracteres" + }, + "hideCharacterCount": { + "message": "Esconder contagem de caracteres" + }, + "itemsInTrash": { + "message": "Itens na lixeira" + }, + "noItemsInTrash": { + "message": "Nenhum item na lixeira" + }, + "noItemsInTrashDesc": { + "message": "Os itens que você excluir aparecerão aqui e serão excluídos permanentemente após 30 dias" + }, + "trashWarning": { + "message": "Os itens que ficarem na lixeira por mais de 30 dias serão excluídos automaticamente" + }, + "restore": { + "message": "Restaurar" + }, + "deleteForever": { + "message": "Apagar permanentemente" + }, + "noEditPermissions": { + "message": "Você não tem permissão para editar este arquivo" } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 0e412729422..75026c9d3e0 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Inicie sessão ou crie uma nova conta para aceder ao seu cofre seguro." }, + "inviteAccepted": { + "message": "Convite aceite" + }, "createAccount": { "message": "Criar conta" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Termine a criação da sua conta definindo uma palavra-passe" }, - "login": { - "message": "Iniciar sessão" - }, "enterpriseSingleSignOn": { "message": "Início de sessão único para empresas" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Dica da palavra-passe mestra (opcional)" }, + "joinOrganization": { + "message": "Aderir à organização" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Conclua a adesão a esta organização ao definir uma palavra-passe mestra." + }, "tab": { "message": "Separador" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Copiar código de segurança" }, + "copyName": { + "message": "Copiar nome" + }, + "copyCompany": { + "message": "Copiar empresa" + }, + "copySSN": { + "message": "Copiar número de segurança social" + }, + "copyPassportNumber": { + "message": "Copiar número do passaporte" + }, + "copyLicenseNumber": { + "message": "Copiar número da carta de condução" + }, "autoFill": { "message": "Preencher automaticamente" }, @@ -280,6 +301,24 @@ "editFolder": { "message": "Editar pasta" }, + "newFolder": { + "message": "Nova pasta" + }, + "folderName": { + "message": "Nome da pasta" + }, + "folderHintText": { + "message": "Aninhe uma pasta adicionando o nome da pasta principal seguido de um \"/\". Exemplo: Redes Sociais/Fóruns" + }, + "noFoldersAdded": { + "message": "Nenhuma pasta adicionada" + }, + "createFoldersToOrganize": { + "message": "Crie pastas para organizar os itens do seu cofre" + }, + "deleteFolderPermanently": { + "message": "Tem a certeza de que pretende eliminar permanentemente esta pasta?" + }, "deleteFolder": { "message": "Eliminar pasta" }, @@ -345,16 +384,56 @@ "message": "Comprimento mínimo da palavra-passe" }, "uppercase": { - "message": "Maiúsculas (A-Z)" + "message": "Maiúsculas (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Minúsculas (a-z)" + "message": "Minúsculas (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Números (0-9)" + "message": "Números (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Caracteres especiais (!@#$%^&*)" + "message": "Caracteres especiais (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Incluir", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Incluir caracteres em maiúsculas", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Incluir caracteres em minúsculas", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Incluir números", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Incluir caracteres especiais", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Número de palavras" @@ -376,7 +455,12 @@ "message": "Mínimo de caracteres especiais" }, "avoidAmbChar": { - "message": "Evitar caracteres ambíguos" + "message": "Evitar caracteres ambíguos", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Evitar caracteres ambíguos", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Procurar no cofre" @@ -556,17 +640,29 @@ "security": { "message": "Segurança" }, + "confirmMasterPassword": { + "message": "Confirmar a palavra-passe mestra" + }, + "masterPassword": { + "message": "Palavra-passe mestra" + }, + "masterPassImportant": { + "message": "A sua palavra-passe mestra não pode ser recuperada se a esquecer!" + }, + "masterPassHintLabel": { + "message": "Dica da palavra-passe mestra" + }, "errorOccurred": { "message": "Ocorreu um erro" }, "emailRequired": { - "message": "É necessário o endereço de e-mail." + "message": "O endereço de e-mail é obrigatório." }, "invalidEmail": { "message": "Endereço de e-mail inválido." }, "masterPasswordRequired": { - "message": "É necessária a palavra-passe mestra." + "message": "A palavra-passe mestra é obrigatória." }, "confirmMasterPasswordRequired": { "message": "É necessário reescrever a palavra-passe mestra." @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "A sua nova conta foi criada! Pode agora iniciar sessão." }, + "newAccountCreated2": { + "message": "A sua nova conta foi criada!" + }, + "youHaveBeenLoggedIn": { + "message": "Iniciou sessão!" + }, "youSuccessfullyLoggedIn": { "message": "Iniciou sessão com sucesso" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "É necessário o código de verificação." }, + "webauthnCancelOrTimeout": { + "message": "A autenticação foi cancelada ou demorou demasiado tempo. Por favor, tente novamente." + }, "invalidVerificationCode": { "message": "Código de verificação inválido" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Digitalize o código QR do autenticador a partir da página Web atual" }, + "totpHelperTitle": { + "message": "Torne a verificação de dois passos simples" + }, + "totpHelper": { + "message": "O Bitwarden pode armazenar e preencher códigos de verificação de dois passos. Copie e cole a chave neste campo." + }, + "totpHelperWithCapture": { + "message": "O Bitwarden pode armazenar e preencher códigos de verificação de dois passos. Selecione o ícone da câmara para tirar uma captura de ecrã do código QR do autenticador deste site ou copie e cole a chave neste campo." + }, + "learnMoreAboutAuthenticators": { + "message": "Saiba mais sobre os autenticadores" + }, "copyTOTP": { "message": "Copiar Chave de autenticação (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "A sua sessão expirou." }, + "logIn": { + "message": "Iniciar sessão" + }, + "restartRegistration": { + "message": "Reiniciar registo" + }, + "expiredLink": { + "message": "Link expirado" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Por favor, reinicie o registo ou tente iniciar sessão." + }, + "youMayAlreadyHaveAnAccount": { + "message": "É possível que já tenha uma conta" + }, "logOutConfirmation": { "message": "Tem a certeza de que pretende terminar sessão?" }, @@ -649,7 +781,7 @@ "message": "Ocorreu um erro inesperado." }, "nameRequired": { - "message": "É necessário o nome." + "message": "O nome é obrigatório." }, "addedFolder": { "message": "Pasta adicionada" @@ -697,6 +829,10 @@ "newUri": { "message": "Novo URI" }, + "addDomain": { + "message": "Adicionar domínio", + "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": { "message": "Item adicionado" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Pedir para adicionar credencial" }, + "vaultSaveOptionsTitle": { + "message": "Guardar nas opções do cofre" + }, "addLoginNotificationDesc": { "message": "Pedir para adicionar um item se não o encontrar no seu cofre." }, "addLoginNotificationDescAlt": { "message": "Pedir para adicionar um item se não for encontrado um no seu cofre. Aplica-se a todas as contas com sessão iniciada." }, + "showCardsInVaultView": { + "message": "Mostrar cartões como sugestões de preenchimento automático na vista do cofre" + }, "showCardsCurrentTab": { "message": "Mostrar cartões na página Separador" }, "showCardsCurrentTabDesc": { "message": "Listar itens de cartões na página Separador para facilitar o preenchimento automático." }, + "showIdentitiesInVaultView": { + "message": "Mostrar identidades como sugestões de preenchimento automático na vista do cofre" + }, "showIdentitiesCurrentTab": { "message": "Mostrar identidades na página Separador" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Deteção de correspondência de URI predefinida", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Escolha a forma predefinida como a deteção de correspondência de URI é tratada para credenciais ao executar ações como o preenchimento automático." @@ -819,7 +964,7 @@ "message": "Tema" }, "themeDesc": { - "message": "Alterar o tema de cores da aplicação." + "message": "Altere o tema de cores da aplicação." }, "themeDescAlt": { "message": "Altere o tema de cores da aplicação. Aplica-se a todas as contas com sessão iniciada." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB de armazenamento encriptado para anexos de ficheiros." }, + "premiumSignUpEmergency": { + "message": "Acesso de emergência." + }, "premiumSignUpTwoStepOptions": { "message": "Opções proprietárias de verificação de dois passos, como YubiKey e Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Pode adquirir uma subscrição Premium no cofre web em bitwarden.com. Pretende visitar o site agora?" }, + "premiumPurchaseAlertV2": { + "message": "Pode adquirir o Premium a partir das definições da sua conta na aplicação Web do Bitwarden." + }, "premiumCurrentMember": { "message": "É um membro Premium!" }, "premiumCurrentMemberThanks": { "message": "Obrigado por apoiar o Bitwarden." }, + "premiumFeatures": { + "message": "Atualize para o Premium e receba:" + }, "premiumPrice": { "message": "Tudo por apenas $PRICE$ /ano!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "Tudo por apenas $PRICE$ por ano!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Atualização concluída" }, @@ -1082,7 +1245,7 @@ "message": "Abrir novo separador" }, "webAuthnAuthenticate": { - "message": "Autenticar WebAuthn" + "message": "Autenticar o WebAuthn" }, "loginUnavailable": { "message": "Início de sessão indisponível" @@ -1179,9 +1342,18 @@ }, "showAutoFillMenuOnFormFields": { "message": "Mostrar menu de preenchimento automático nos campos do formulário", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Sugestões de preenchimento automático" + }, + "showInlineMenuLabel": { + "message": "Mostrar sugestões de preenchimento automático nos campos do formulário" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Apresentar sugestões quando o ícone é selecionado" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Aplica-se a todas as contas com sessão iniciada." }, "turnOffBrowserBuiltInPasswordManagerSettings": { @@ -1202,15 +1374,34 @@ "message": "Quando o ícone de preenchimento automático está selecionado", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Preencher automaticamente ao carregar a página" + }, "enableAutoFillOnPageLoad": { "message": "Preencher automaticamente ao carregar a página" }, "enableAutoFillOnPageLoadDesc": { "message": "Se for detetado um formulário de início de sessão, o preenchimento automático é efetuado quando a página Web é carregada." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Aviso:$CLOSETAG$ Aviso: Os sites comprometidos ou não confiáveis podem explorar o preenchimento automático ao carregar a página.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Os sites comprometidos ou não confiáveis podem explorar o preenchimento automático ao carregar a página." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Saber mais sobre os riscos" + }, "learnMoreAboutAutofill": { "message": "Saber mais sobre o preenchimento automático" }, @@ -1238,9 +1429,15 @@ "commandOpenSidebar": { "message": "Abrir o cofre na barra lateral" }, - "commandAutofillDesc": { + "commandAutofillLoginDesc": { "message": "Preencher automaticamente com a última credencial utilizada no site atual" }, + "commandAutofillCardDesc": { + "message": "Preencher automaticamente com o último cartão utilizado no site atual" + }, + "commandAutofillIdentityDesc": { + "message": "Preencher automaticamente com a última identidade utilizada no site atual" + }, "commandGeneratePasswordDesc": { "message": "Gerar e copiar uma nova palavra-passe aleatória para a área de transferência" }, @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Booleano" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Associado", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "Ver $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Histórico de palavras-passe" }, @@ -1533,6 +1742,10 @@ "message": "Domínio de base", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Domínio base (recomendado)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Nome do domínio", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Deteção de correspondência", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Deteção de correspondência predefinida", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Alternar opções" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Não existem palavras-passe para listar." }, + "clearHistory": { + "message": "Limpar histórico" + }, + "noPasswordsToShow": { + "message": "Não há palavras-passe para mostrar" + }, + "noRecentlyGeneratedPassword": { + "message": "Não gerou nenhuma palavra-passe recentemente" + }, "remove": { "message": "Remover" }, @@ -1677,6 +1899,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" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "A sua nova palavra-passe mestra não cumpre os requisitos da política." }, - "receiveMarketingEmails": { - "message": "Receba e-mails do Bitwarden com anúncios, conselhos e oportunidades de investigação." + "receiveMarketingEmailsV2": { + "message": "Receba conselhos, anúncios e oportunidades de investigação do Bitwarden na sua caixa de entrada." }, "unsubscribe": { "message": "Anular subscrição" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Por favor, desbloqueie este utilizador na aplicação para computador e tente novamente." }, + "biometricsNotAvailableTitle": { + "message": "Desbloqueio biométrico indisponível" + }, + "biometricsNotAvailableDesc": { + "message": "O desbloqueio biométrico não está atualmente disponível. Por favor, tente novamente mais tarde." + }, "biometricsFailedTitle": { "message": "Falha na biometria" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "Uma política da organização bloqueou a importação de itens para o seu cofre individual." }, + "domainsTitle": { + "message": "Domínios", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Domínios excluídos" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "O Bitwarden não pedirá para guardar os detalhes de início de sessão destes domínios para todas as contas com sessão iniciada. É necessário atualizar a página para que as alterações tenham efeito." }, + "websiteItemLabel": { + "message": "Site $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ não é um domínio válido", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Alterações do domínio excluído guardadas" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Protegido por palavra-passe" }, + "copyLink": { + "message": "Copiar link" + }, "copySendLink": { "message": "Copiar link do Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send criado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send criado com sucesso!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "O Send estará disponível para qualquer pessoa que tenha o link durante os próximos $DAYS$ dias.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Link do Send copiado", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send editado", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Verificação de e-mail necessária" }, + "emailVerifiedV2": { + "message": "E-mail verificado" + }, "emailVerificationRequiredDesc": { "message": "Tem de verificar o seu e-mail para utilizar esta funcionalidade. Pode verificar o seu e-mail no cofre Web." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Inscrição automática" }, @@ -2274,7 +2574,7 @@ } }, "leaveOrganization": { - "message": "Deixar a organização" + "message": "Sair da organização" }, "removeMasterPassword": { "message": "Remover palavra-passe mestra" @@ -2283,7 +2583,7 @@ "message": "Palavra-passe mestra removida" }, "leaveOrganizationConfirmation": { - "message": "Tem a certeza de que pretende deixar esta organização?" + "message": "Tem a certeza de que pretende sair desta organização?" }, "leftOrganization": { "message": "Saiu da organização." @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Definições de preenchimento automático" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Atalho de preenchimento automático" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Alterar atalho" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Gerir atalhos" + }, "autofillShortcut": { "message": "Atalho de teclado de preenchimento automático" }, - "autofillShortcutNotSet": { - "message": "O atalho de preenchimento automático não está definido. Altere-o nas definições do navegador." + "autofillLoginShortcutNotSet": { + "message": "O atalho de preenchimento automático de credenciais não está definido. Altere-o nas definições do navegador." }, - "autofillShortcutText": { - "message": "O atalho de preenchimento automático é: $COMMAND$. Altere-o nas definições do navegador.", + "autofillLoginShortcutText": { + "message": "O atalho de preenchimento automático de credenciais é $COMMAND$. Gira todos os atalhos nas definidções do navegador.", "placeholders": { "command": { "content": "$1", @@ -2738,11 +3047,19 @@ "deviceTrusted": { "message": "Dispositivo de confiança" }, + "sendsNoItemsTitle": { + "message": "Sem Sends ativos", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Utilize o Send para partilhar de forma segura informações encriptadas com qualquer pessoa.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { - "message": "Campo necessário." + "message": "Campo obrigatório." }, "required": { - "message": "necessário" + "message": "obrigatório" }, "search": { "message": "Procurar" @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 campo precisa da sua atenção." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ campos precisam da sua atenção.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Selecionar --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Os itens que voltem a pedir a palavra-passe mestra não podem ser preenchidos automaticamente no carregamento da página. Preenchimento automático no carregamento da página desativado.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "Preencher automaticamente ao carregar a página definido para utilizar a predefinição.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Desativar o pedido para reintroduzir a palavra-passe mestra para editar este campo", @@ -2896,7 +3225,7 @@ "message": "Avançar para o conteúdo" }, "bitwardenOverlayButton": { - "message": "Botão de menu de preenchimento automático Bitwarden", + "message": "Botão de menu de preenchimento automático do Bitwarden", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { @@ -2911,10 +3240,18 @@ "message": "Desbloqueie a sua conta para ver as credenciais correspondentes", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Desbloqueie a sua conta para ver sugestões de preenchimento automático", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Desbloquear a conta", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Desbloqueie a sua conta, abre numa nova janela", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Preencher as credenciais para", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Adicionar novo item do cofre", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "Nova credencial", + "description": "Button text to display within inline menu when there are no matching items on a login field" + }, + "addNewLoginItemAria": { + "message": "Adicione uma nova credencial ao cofre, abre numa nova janela", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "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": "Adicione um novo cartão ao cofre, abre numa nova janela", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "Nova identidade", + "description": "Button text to display within inline menu when there are no matching items on an identity field" + }, + "addNewIdentityItemAria": { + "message": "Adicione uma nova identidade ao cofre, abre numa nova janela", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Menu de preenchimento automático Bitwarden disponível. Prima a tecla de seta para baixo para selecionar.", + "message": "Menu de preenchimento automático do Bitwarden disponível. Prima a tecla de seta para baixo para selecionar.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Erro ao ligar ao serviço Duo. Utilize um método de verificação de dois passos diferente ou contacte o Duo para obter assistência." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Inicie o Duo e siga os passos para concluir o início de sessão." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Palavra-passe de ficheiro inválida, utilize a palavra-passe que introduziu quando criou o ficheiro de exportação." }, - "importDestination": { - "message": "Destino da importação" + "destination": { + "message": "Destino" }, "learnAboutImportOptions": { "message": "Saiba mais sobre as suas opções de importação" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verificação exigida pelo site inicial. Esta funcionalidade ainda não está implementada para contas sem palavra-passe mestra." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Iniciar sessão com a chave de acesso?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "Não tem uma credencial correspondente para este site." }, + "noMatchingLoginsForSite": { + "message": "Sem credenciais correspondentes para este site" + }, "confirm": { "message": "Confirmar" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Guardar a chave de acesso como uma nova credencial" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Escolha uma credencial para guardar esta chave de acesso" }, + "chooseCipherForPasskeyAuth": { + "message": "Escolha uma chave de acesso para iniciar sessão" + }, "passkeyItem": { "message": "Item da chave de acesso" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Formatos comuns", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continuar para as definições do navegador?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "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": "Altere as definições de preenchimento automático e de gestão de palavras-passe 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": "Pode ver e definir atalhos de extensão nas definiçõ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": "Altere as definições de preenchimento automático e de gestão de palavras-passe 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": "Pode ver e definir atalhos de extensão nas definiçõ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": { "message": "Tornar o Bitwarden o seu gestor de palavras-passe predefinido?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignorar esta opção pode causar conflitos entre o menu de preenchimento automático do Bitwarden e o do seu navegador.", + "message": "Ignorar esta opção pode causar conflitos entre as sugestões de preenchimento automático do Bitwarden e as do seu navegador.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credenciais guardadas com sucesso!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Palavra-passe guardada!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credenciais atualizadas com sucesso!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Palavra passe atualizada!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Erro ao guardar as credenciais. Verifique a consola para obter detalhes.", "description": "Notification message for when saving credentials has failed." @@ -3334,20 +3736,6 @@ "passkeyRemoved": { "message": "Chave de acesso removida" }, - "unassignedItemsBannerNotice": { - "message": "Aviso: Os itens da organização não atribuídos já não são visíveis na vista Todos os cofres e só são acessíveis através da Consola de administração." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Aviso: A 16 de maio de 2024, os itens da organização não atribuídos deixarão de estar visíveis na vista Todos os cofres e só estarão acessíveis através da consola de administração." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Atribua estes itens a uma coleção a partir da", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "para os tornar visíveis.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Sugestões de preenchimento automático" }, @@ -3415,7 +3803,7 @@ }, "autofillTitle": { "message": "Preencher automaticamente - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "Não há valores a copiar" }, - "assignCollections": { - "message": "Atribuir coleções" + "assignToCollections": { + "message": "Atribuir às coleções" }, "copyEmail": { "message": "Copiar e-mail" @@ -3493,13 +3881,13 @@ "message": "Itens sem pasta" }, "itemDetails": { - "message": "Item details" + "message": "Detalhes do item" }, "itemName": { - "message": "Item name" + "message": "Nome do item" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Não é possível remover coleções com permissões de Apenas visualização: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3899,33 @@ "message": "A organização está desativada" }, "owner": { - "message": "Owner" + "message": "Proprietário" }, "selfOwnershipLabel": { - "message": "You", + "message": "Eu", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Não é possível aceder aos itens de organizações desativadas. Contacte o proprietário da organização para obter assistência." }, + "additionalInformation": { + "message": "Informações adicionais" + }, + "itemHistory": { + "message": "Histórico do item" + }, + "lastEdited": { + "message": "Última edição" + }, + "ownerYou": { + "message": "Proprietário: Eu" + }, + "linked": { + "message": "Associado" + }, + "copySuccessful": { + "message": "Cópia bem-sucedida" + }, "upload": { "message": "Carregar" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filtros" + }, + "personalDetails": { + "message": "Dados pessoais" + }, + "identification": { + "message": "Identificação" + }, + "contactInfo": { + "message": "Informações de contacto" + }, + "downloadAttachment": { + "message": "Transferir - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "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": { + "message": "Credenciais de início de sessão" + }, + "authenticatorKey": { + "message": "Chave de autenticação" + }, + "autofillOptions": { + "message": "Opções de preenchimento automático" + }, + "websiteUri": { + "message": "Site (URI)" + }, + "websiteUriCount": { + "message": "Site (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": "Site adicionado" + }, + "addWebsite": { + "message": "Adicionar site" + }, + "deleteWebsite": { + "message": "Eliminar site" + }, + "defaultLabel": { + "message": "Predefinido ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Mostrar deteção de correspondência para $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Ocultar deteção de correspondência para $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Preencher automaticamente ao carregar a página?" + }, + "cardExpiredTitle": { + "message": "Cartão expirado" + }, + "cardExpiredMessage": { + "message": "Se o renovou, atualize as informações do cartão" + }, + "cardDetails": { + "message": "Detalhes do cartão" + }, + "cardBrandDetails": { + "message": "Detalhes do $BRAND$", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Ativar animações" + }, + "addAccount": { + "message": "Adicionar conta" + }, + "loading": { + "message": "A carregar" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Apenas os membros da organização com acesso a estas coleções poderão ver o item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Apenas os membros da organização com acesso a estas coleções poderão ver os itens." + }, + "bulkCollectionAssignmentWarning": { + "message": "Selecionou $TOTAL_COUNT$ itens. Não pode atualizar $READONLY_COUNT$ dos itens porque não tem permissões de edição.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Adicionar campo" + }, + "add": { + "message": "Adicionar" + }, + "fieldType": { + "message": "Tipo de campo" + }, + "fieldLabel": { + "message": "Etiqueta do campo" + }, + "textHelpText": { + "message": "Utilize campos de texto para dados como perguntas de segurança" + }, + "hiddenHelpText": { + "message": "Utilize campos ocultos para dados sensíveis como uma palavra-passe" + }, + "checkBoxHelpText": { + "message": "Utilize caixas de verificação se pretender preencher automaticamente uma caixa de verificação de um formulário, como um e-mail de memorização" + }, + "linkedHelpText": { + "message": "Utilize um campo ligado quando tiver problemas de preenchimento automático para um site específico." + }, + "linkedLabelHelpText": { + "message": "Introduza o ID do HTML, o nome, a aria-label ou o placeholder do campo." + }, + "editField": { + "message": "Editar campo" + }, + "editFieldLabel": { + "message": "Editar $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Eliminar $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ adicionado", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reordenar $LABEL$. Utilize a tecla de seta para mover o item para cima ou para baixo.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ movido para cima, posição $INDEX$ de $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Selecione as coleções a atribuir" + }, + "personalItemTransferWarningSingular": { + "message": "1 será permanentemente transferido para a organização selecionada. Este item deixará de lhe pertencer." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ itens serão permanentemente transferidos para a organização selecionada. Estes itens deixarão de lhe pertencer.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 será permanentemente transferido para a $ORG$. Este item deixará de lhe pertencer.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ itens serão permanentemente transferidos para $ORG$. Estes itens deixarão de lhe pertencer.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Coleções atribuídas com sucesso" + }, + "nothingSelected": { + "message": "Não selecionou nada." + }, + "movedItemsToOrg": { + "message": "Itens selecionados movidos para $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Itens movidos para $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item movido para $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ movido para baixo, posição $INDEX$ de $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Localização do item" + }, + "fileSends": { + "message": "Sends de ficheiros" + }, + "textSends": { + "message": "Sends de texto" + }, + "bitwardenNewLook": { + "message": "O Bitwarden tem um novo visual!" + }, + "bitwardenNewLookDesc": { + "message": "É mais fácil e mais intuitivo do que nunca preencher automaticamente e pesquisar a partir do separador Cofre. Dê uma vista de olhos!" + }, + "accountActions": { + "message": "Ações da conta" + }, + "showNumberOfAutofillSuggestions": { + "message": "Mostrar o número de sugestões de preenchimento automático de credenciais no ícone da extensão" + }, + "systemDefault": { + "message": "Predefinição do sistema" + }, + "enterprisePolicyRequirementsApplied": { + "message": "Os requisitos da política empresarial foram aplicados a esta definição" + }, + "fileSavedToDevice": { + "message": "Ficheiro guardado no dispositivo. Gira-o a partir das transferências do seu dispositivo." + }, + "showCharacterCount": { + "message": "Mostrar contagem de caracteres" + }, + "hideCharacterCount": { + "message": "Ocultar contagem de caracteres" + }, + "itemsInTrash": { + "message": "Itens no lixo" + }, + "noItemsInTrash": { + "message": "Nenhum item no lixo" + }, + "noItemsInTrashDesc": { + "message": "Os itens que eliminar aparecerão aqui e serão permanentemente eliminados após 30 dias" + }, + "trashWarning": { + "message": "Os itens que estiverem no lixo há mais de 30 dias serão automaticamente eliminados" + }, + "restore": { + "message": "Restaurar" + }, + "deleteForever": { + "message": "Eliminar para sempre" + }, + "noEditPermissions": { + "message": "Não tem permissão para editar este item" } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 6fb7e5857c3..989f5e1b2ee 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": "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" - }, - "login": { - "message": "Conectare" + "message": "Finalizați crearea contului prin setarea unei parole" }, "enterpriseSingleSignOn": { "message": "Conectare unică organizație" @@ -50,7 +50,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", @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Indiciu pentru parola principală (opțional)" }, + "joinOrganization": { + "message": "Alăturați-vă organizației" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finalizați aderarea la această organizație prin setarea unei parole principale." + }, "tab": { "message": "Filă" }, @@ -107,17 +113,32 @@ "copySecurityCode": { "message": "Copiere cod de securitate" }, + "copyName": { + "message": "Copiați numele" + }, + "copyCompany": { + "message": "Copiați firma" + }, + "copySSN": { + "message": "Copiați numărul de securitate socială" + }, + "copyPassportNumber": { + "message": "Copiați numărul pașaportului" + }, + "copyLicenseNumber": { + "message": "Copiați numărul de licență" + }, "autoFill": { "message": "Auto-completare" }, "autoFillLogin": { - "message": "Auto-fill login" + "message": "Autocompletare date de autentificare" }, "autoFillCard": { - "message": "Auto-fill card" + "message": "Autocompletare card" }, "autoFillIdentity": { - "message": "Auto-fill identity" + "message": "Autocompletare identitate" }, "generatePasswordCopied": { "message": "Generare parolă (s-a copiat)" @@ -129,19 +150,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" @@ -189,25 +210,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ă", @@ -224,43 +245,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" @@ -280,6 +301,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" }, @@ -321,7 +360,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" @@ -342,19 +381,59 @@ "message": "Lungime" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "Lungimea minimă a parolei" }, "uppercase": { - "message": "Litere mari (A-Z)" + "message": "Litere mari (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Litere mici (a-z)" + "message": "Litere mici (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numere (0-9)" + "message": "Numere (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Caractere speciale (!@#$%^&*)" + "message": "Caractere speciale (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Număr de cuvinte" @@ -376,7 +455,12 @@ "message": "Minim de caractere speciale" }, "avoidAmbChar": { - "message": "Se evită caracterele ambigue" + "message": "Se evită caracterele ambigue", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Căutare în seif" @@ -400,7 +484,7 @@ "message": "Parolă" }, "totp": { - "message": "Authenticator secret" + "message": "Cheie de autentificare" }, "passphrase": { "message": "Frază de acces" @@ -409,13 +493,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" @@ -439,7 +523,7 @@ "message": "Lansare" }, "launchWebsite": { - "message": "Launch website" + "message": "Lansați siteul web" }, "website": { "message": "Sait web" @@ -454,19 +538,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" @@ -509,7 +593,7 @@ "message": "Blocare imediată" }, "lockAll": { - "message": "Lock all" + "message": "Blochează toate" }, "immediately": { "message": "Imediat" @@ -556,6 +640,18 @@ "security": { "message": "Securitate" }, + "confirmMasterPassword": { + "message": "Confirmați parola principală" + }, + "masterPassword": { + "message": "Parola principală" + }, + "masterPassImportant": { + "message": "Parola principală nu poate fi recuperată dacă este uitată!" + }, + "masterPassHintLabel": { + "message": "Indiciu pentru parola principală" + }, "errorOccurred": { "message": "S-a produs o eroare" }, @@ -587,11 +683,17 @@ "newAccountCreated": { "message": "Noul dvs. cont a fost creat! Acum vă puteți autentifica." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "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." @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Este necesar codul de verificare." }, + "webauthnCancelOrTimeout": { + "message": "Autentificarea a fost anulată sau a luat prea mult. Încercați din nou." + }, "invalidVerificationCode": { "message": "Cod de verificare nevalid" }, @@ -616,26 +721,53 @@ "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": "Faceți autentificarea in 2 pași mai ușoară" + }, + "totpHelper": { + "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 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." + }, + "learnMoreAboutAuthenticators": { + "message": "Learn more about authenticators" }, "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": "Autentificare" + }, + "restartRegistration": { + "message": "Reporniți înregistrarea" + }, + "expiredLink": { + "message": "Link expirat" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Vă rugăm să reporniți înregistrarea sau să încercați să vă conectați." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Este posibil să aveți deja un cont" + }, "logOutConfirmation": { "message": "Sigur doriți să vă deconectați?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "URI nou" }, + "addDomain": { + "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": { "message": "Articol adăugat" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Solicitare de adăugare cont" }, + "vaultSaveOptionsTitle": { + "message": "Salvare în opțiuni seif" + }, "addLoginNotificationDesc": { "message": "Solicitați adăugarea unui element dacă nu se găsește unul în seif." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Afișați cardurile pe pagina Filă" }, "showCardsCurrentTabDesc": { "message": "Listați elementele cardului pe pagina Filă pentru a facilita completarea automată." }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" + }, "showIdentitiesCurrentTab": { "message": "Afișați identitățile pe pagina Filă" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Detectare implicită a potrivirii URI", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Alege modul implicit de gestionare a detectării de potrivire URI pentru conectări când se efectuează acțiuni precum auto-completarea." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB spațiu de stocare criptat pentru atașamente de fișiere." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Opțiuni brevetate de conectare cu doi factori, cum ar fi YubiKey și Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Puteți achiziționa un abonament Premium pe website-ul bitwarden.com. Doriți să vizitați site-ul acum?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Sunteți un membru Premium!" }, "premiumCurrentMemberThanks": { "message": "Vă mulțumim pentru susținerea Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "Totul pentru doar %price% /an!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Actualizare completă" }, @@ -1178,14 +1341,23 @@ "message": "URL-urile mediului au fost salvate" }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,18 +1371,37 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "Completare automată la încărcarea paginii" }, "enableAutoFillOnPageLoadDesc": { "message": "Dacă se detectează un formular de autentificare, completați-l automat la încărcarea paginii web." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Site-urile web compromise sau nesigure pot profita de auto-completarea la încărcare." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" + }, "learnMoreAboutAutofill": { "message": "Mai multe informații despre auto-completare" }, @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Deschidere seif în bara laterală" }, - "commandAutofillDesc": { - "message": "Auto-completare a ultimei autentificări utilizate pe saitul web curent" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Generare parolă aleatorie și copiere în clipboard" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Valoare logică" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Conectat", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Istoric parole" }, @@ -1533,6 +1742,10 @@ "message": "Domeniu de bază", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Nume de domeniu", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Detectare de potrivire", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Detectare de potrivire implicită", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Comutare opțiuni" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Nicio parolă de afișat." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Ștergere" }, @@ -1677,6 +1899,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" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Noua dvs. parolă principală nu îndeplinește cerințele politicii." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrica a eșuat" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Domenii excluse" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ nu este un domeniu valid", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Protejat cu parolă" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Copiere link Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send creat", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send salvat", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Verificare e-mail necesară" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Trebuie să vă verificați e-mailul pentru a utiliza această caracteristică. Puteți verifica e-mailul în seiful web." }, @@ -2179,6 +2476,9 @@ "updateWeakMasterPasswordWarning": { "message": "Parola dvs. principală nu respectă una sau mai multe politici ale organizației. Pentru a accesa seiful, parola principală trebuie actualizată acum. În cazul în care continuați, veți fi deconectat din sesiunea curentă și va trebui să vă conectați din nou. Sesiunile active de pe alte dispozitive pot rămâne active timp de până la o oră." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "resetPasswordPolicyAutoEnroll": { "message": "Înscrierea automată" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Setări de auto-completare" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" + }, "autofillShortcut": { "message": "Scurtătură de tastatură pentru auto-completare" }, - "autofillShortcutNotSet": { - "message": "Scurtătura de auto-completare nu este setată. Modificați acest lucru în setările browserului." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "Scurtătura de auto-completare este: $COMMAND$. Modificați acest lucru în setările browserului.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Dispozitiv de încredere" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Este necesară o intrare." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Selectați --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Elementele în care parola principală este solicitată din nou nu pot fi completate automat la încărcarea paginii. Completarea automată la încărcarea paginii este dezactivată.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "Completarea automată la încărcarea paginii este setată la valoarea implicită.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Dezactivați reintroducerea parolei principale pentru a edita acest câmp", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 74f2166aa8c..31d419aea60 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Войдите или создайте новый аккаунт для доступа к вашему защищенному хранилищу." }, + "inviteAccepted": { + "message": "Приглашение принято" + }, "createAccount": { "message": "Создать аккаунт" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Завершите создание аккаунта, задав пароль" }, - "login": { - "message": "Войти" - }, "enterpriseSingleSignOn": { "message": "Единая корпоративная авторизация" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Подсказка к мастер-паролю (необяз.)" }, + "joinOrganization": { + "message": "Присоединиться к организации" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Завершите присоединение к этой организации, установив мастер-пароль." + }, "tab": { "message": "Вкладка" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Скопировать код безопасности" }, + "copyName": { + "message": "Скопировать название" + }, + "copyCompany": { + "message": "Скопировать компанию" + }, + "copySSN": { + "message": "Скопировать номер социального страхования" + }, + "copyPassportNumber": { + "message": "Скопировать номер паспорта" + }, + "copyLicenseNumber": { + "message": "Скопировать номер лицензии" + }, "autoFill": { "message": "Автозаполнение" }, @@ -280,6 +301,24 @@ "editFolder": { "message": "Изменить папку" }, + "newFolder": { + "message": "Новый папка" + }, + "folderName": { + "message": "Название папки" + }, + "folderHintText": { + "message": "Создайте вложенную папку, добавив название родительской папки и символ \"/\". Пример: Сообщества/Форумы" + }, + "noFoldersAdded": { + "message": "Нет добавленных папок" + }, + "createFoldersToOrganize": { + "message": "Создавайте папки для упорядочивания элементов хранилища" + }, + "deleteFolderPermanently": { + "message": "Вы действительно хотите безвозвратно удалить эту папку?" + }, "deleteFolder": { "message": "Удалить папку" }, @@ -345,16 +384,56 @@ "message": "Минимальная длина пароля" }, "uppercase": { - "message": "Прописные буквы (A-Z)" + "message": "Прописные буквы (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Строчные буквы (a-z)" + "message": "Строчные буквы (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Цифры (0-9)" + "message": "Цифры (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Специальные символы (!@#$%^&*)" + "message": "Специальные символы (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Включить", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Включить заглавные символы", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Включить строчные символы", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Включить цифры", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Включить специальные символы", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Количество слов" @@ -376,7 +455,12 @@ "message": "Минимум символов" }, "avoidAmbChar": { - "message": "Избегать неоднозначных символов" + "message": "Избегать неоднозначных символов", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Избегать неоднозначных символов", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Поиск в хранилище" @@ -556,6 +640,18 @@ "security": { "message": "Безопасность" }, + "confirmMasterPassword": { + "message": "Подтвердите мастер-пароль" + }, + "masterPassword": { + "message": "Мастер-пароль" + }, + "masterPassImportant": { + "message": "Ваш мастер-пароль невозможно восстановить, если вы его забудете!" + }, + "masterPassHintLabel": { + "message": "Подсказка к мастер-паролю" + }, "errorOccurred": { "message": "Произошла ошибка" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Ваш аккаунт создан! Теперь вы можете войти в систему." }, + "newAccountCreated2": { + "message": "Ваш новый аккаунт создан!" + }, + "youHaveBeenLoggedIn": { + "message": "Вы авторизовались!" + }, "youSuccessfullyLoggedIn": { "message": "Вы успешно авторизовались" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Необходимо ввести код подтверждения." }, + "webauthnCancelOrTimeout": { + "message": "Аутентификация была отменена или заняла слишком много времени. Пожалуйста, попробуйте еще раз." + }, "invalidVerificationCode": { "message": "Неверный код подтверждения" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Сканировать QR-код аутентификатора с текущей веб-страницы" }, + "totpHelperTitle": { + "message": "Сделайте двухэтапную аутентификацию простой и удобной" + }, + "totpHelper": { + "message": "Bitwarden может хранить и заполнять коды двухэтапной аутентификации. Скопируйте и вставьте ключ в это поле." + }, + "totpHelperWithCapture": { + "message": "Bitwarden может хранить и заполнять коды двухэтапной аутентификации. Выберите значок камеры, чтобы сделать скриншот QR-кода этого сайта, или скопируйте и вставьте ключ в это поле." + }, + "learnMoreAboutAuthenticators": { + "message": "Узнайте больше об аутентификаторах" + }, "copyTOTP": { "message": "Скопировать ключ аутентификатора (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Истек срок действия вашего сеанса." }, + "logIn": { + "message": "Войти" + }, + "restartRegistration": { + "message": "Перезапустить регистрацию" + }, + "expiredLink": { + "message": "Истекшая ссылка" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Пожалуйста, перезапустите регистрацию или попробуйте авторизоваться." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Возможно, у вас уже есть аккаунт" + }, "logOutConfirmation": { "message": "Вы действительно хотите выйти?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Новый URI" }, + "addDomain": { + "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": { "message": "Элемент добавлен" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Спрашивать при добавлении логина" }, + "vaultSaveOptionsTitle": { + "message": "Параметры сохранения в хранилище" + }, "addLoginNotificationDesc": { "message": "Запросить добавление элемента, если его нет в вашем хранилище." }, "addLoginNotificationDescAlt": { "message": "Запрос на добавление элемента, если он отсутствует в вашем хранилище. Применяется ко всем авторизованным аккаунтам." }, + "showCardsInVaultView": { + "message": "Показывать карты как предложение автозаполнения при просмотре Хранилище" + }, "showCardsCurrentTab": { "message": "Показывать карты на вкладке" }, "showCardsCurrentTabDesc": { "message": "Карты будут отображены на вкладке для удобного автозаполнения." }, + "showIdentitiesInVaultView": { + "message": "Показывать личности как предложение автозаполнения при просмотре Хранилище" + }, "showIdentitiesCurrentTab": { "message": "Показывать Личности на вкладке" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Обнаружение совпадения URI по умолчанию", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Выберите стандартный способ определения соответствия URI для логинов при выполнении таких действий, как автоматическое заполнение." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 ГБ зашифрованного хранилища для вложенных файлов." }, + "premiumSignUpEmergency": { + "message": "Экстренный доступ" + }, "premiumSignUpTwoStepOptions": { "message": "Проприетарные варианты двухэтапной аутентификации, такие как YubiKey или Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Вы можете купить Премиум на bitwarden.com. Перейти на сайт сейчас?" }, + "premiumPurchaseAlertV2": { + "message": "Премиум можно приобрести в настройках аккаунта в веб-версии Bitwarden." + }, "premiumCurrentMember": { "message": "У вас есть Премиум!" }, "premiumCurrentMemberThanks": { "message": "Благодарим вас за поддержку Bitwarden." }, + "premiumFeatures": { + "message": "Перейдите на Премиум и получите:" + }, "premiumPrice": { "message": "Всего лишь $PRICE$ в год!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "Всего лишь $PRICE$ в год!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Обновление завершено" }, @@ -1179,9 +1342,18 @@ }, "showAutoFillMenuOnFormFields": { "message": "Показывать меню автозаполнения в полях формы", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Предложения по автозаполнению" + }, + "showInlineMenuLabel": { + "message": "Показывать предположения автозаполнения в полях формы" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Показывать подсказки при выборе значка" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Применяется ко всем авторизованным аккаунтам." }, "turnOffBrowserBuiltInPasswordManagerSettings": { @@ -1202,15 +1374,34 @@ "message": "Если выбран значок автозаполнения", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Автозаполнение при загрузке страницы" + }, "enableAutoFillOnPageLoad": { "message": "Автозаполнение при загрузке страницы" }, "enableAutoFillOnPageLoadDesc": { "message": "Если обнаружена форма входа, автозаполнение выполняется при загрузке веб-страницы." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Предупреждение:$CLOSETAG$ взломанные или недоверенные сайты могут использовать автозаполнение при загрузке страницы.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Взломанные или недоверенные сайты могут внедрить вредоносный код во время автозаполнения при загрузке страницы." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Узнайте больше о рисках" + }, "learnMoreAboutAutofill": { "message": "Узнать больше об автозаполнении" }, @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Открыть хранилище в боковой панели" }, - "commandAutofillDesc": { - "message": "Автозаполнение последнего использованного логина для текущего сайта." + "commandAutofillLoginDesc": { + "message": "Автозаполнение последнего использованного логина для текущего сайта" + }, + "commandAutofillCardDesc": { + "message": "Автозаполнение последней использованной карты для текущего сайта" + }, + "commandAutofillIdentityDesc": { + "message": "Автозаполнение последней использованной личности для текущего сайта" }, "commandGeneratePasswordDesc": { "message": "Сгенерировать и скопировать новый случайный пароль в буфер обмена." @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Логическое" }, + "cfTypeCheckbox": { + "message": "Флажок" + }, "cfTypeLinked": { "message": "Связано", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "Просмотр $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "История паролей" }, @@ -1533,6 +1742,10 @@ "message": "Основной домен", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Основной домен (рекомендуется)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Доменное имя", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Обнаружение совпадений", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Метод обнаружения по умолчанию", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Настройки перебора" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Нет паролей для отображения." }, + "clearHistory": { + "message": "Очистить историю" + }, + "noPasswordsToShow": { + "message": "Нет паролей для отображения" + }, + "noRecentlyGeneratedPassword": { + "message": "Нет недавно сгенерированных паролей" + }, "remove": { "message": "Удалить" }, @@ -1677,6 +1899,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": "Действие по тайм-ауту хранилища" }, @@ -1722,7 +1967,7 @@ "message": "Заполнить и сохранить" }, "autoFillSuccessAndSavedUri": { - "message": "URI элемента заполнен и сохранен" + "message": "Элемент заполнен, URI сохранен" }, "autoFillSuccess": { "message": "Элемент заполнен " @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Ваш новый мастер-пароль не соответствует требованиям политики." }, - "receiveMarketingEmails": { - "message": "Получайте электронные письма от Bitwarden с анонсами, советами и возможностями для исследований." + "receiveMarketingEmailsV2": { + "message": "Получайте советы, анонсы и возможности для исследований от Bitwarden на свой email." }, "unsubscribe": { "message": "Отписаться" @@ -1874,6 +2119,12 @@ "nativeMessagingWrongUserTitle": { "message": "Несоответствие аккаунта" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Биометрическая разблокировка не удалась. Биометрический секретный ключ не смог разблокировать хранилище. Пожалуйста, попробуйте настроить биометрию еще раз." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Несовпадение биометрического ключа" + }, "biometricsNotEnabledTitle": { "message": "Биометрия не настроена" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Пожалуйста, разблокируйте этого пользователя в приложении для компьютера и повторите попытку." }, + "biometricsNotAvailableTitle": { + "message": "Разблокировка биометрией недоступна" + }, + "biometricsNotAvailableDesc": { + "message": "Разблокировка биометрией в настоящее время недоступна. Пожалуйста, повторите попытку позже." + }, "biometricsFailedTitle": { "message": "Сбой биометрии" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "Импорт элементов в ваше личное хранилище отключен политикой организации." }, + "domainsTitle": { + "message": "Домены", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Исключенные домены" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden не будет предлагать сохранение логинов для этих доменов для всех авторизованных аккаунтов. Для вступления изменений в силу необходимо обновить страницу." }, + "websiteItemLabel": { + "message": "Сайт $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ – некорректно указанный домен", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Изменения в исключенном домене сохранены" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Защищено паролем" }, + "copyLink": { + "message": "Скопировать ссылку" + }, "copySendLink": { "message": "Скопировать ссылку на Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send создана", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send успешно создана!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "Send будет доступна всем, кто получит ссылку в течение следующих дней: $DAYS$.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Ссылка на Send скопирована", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send сохранена", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Требуется подтверждение электронной почты" }, + "emailVerifiedV2": { + "message": "Email подтвержден" + }, "emailVerificationRequiredDesc": { "message": "Для использования этой функции необходимо подтвердить ваш email. Вы можете это сделать в веб-хранилище." }, @@ -2179,6 +2476,9 @@ "updateWeakMasterPasswordWarning": { "message": "Ваш мастер-пароль не соответствует требованиям политики вашей организации. Для доступа к хранилищу вы должны обновить свой мастер-пароль прямо сейчас. При этом текущий сеанс будет завершен и потребуется повторная авторизация. Сеансы на других устройствах могут оставаться активными в течение часа." }, + "tdeDisabledMasterPasswordRequired": { + "message": "В вашей организации отключено шифрование доверенных устройств. Пожалуйста, установите мастер-пароль для доступа к вашему хранилищу." + }, "resetPasswordPolicyAutoEnroll": { "message": "Автоматическое развертывание" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Настройки автозаполнения" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Ярлык автозаполнения" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Изменить ярлык" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Управление ярлыками" + }, "autofillShortcut": { "message": "Сочетание клавиш для автозаполнения" }, - "autofillShortcutNotSet": { - "message": "Сочетание клавиш для автозаполнения не установлено. Установите его в настройках браузера." + "autofillLoginShortcutNotSet": { + "message": "Сочетание клавиш для автозаполнения логина не установлено. Установите его в настройках браузера." }, - "autofillShortcutText": { - "message": "Сочетание клавиш для автозаполнения: $COMMAND$. Измените его в настройках браузера.", + "autofillLoginShortcutText": { + "message": "Сочетание клавиш для автозаполнения логина - $COMMAND$. Управлять всеми сочетаниями можно в настройках браузера.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Доверенное устройство" }, + "sendsNoItemsTitle": { + "message": "Нет активных Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Используйте Send для безопасного обмена зашифрованной информацией с кем угодно.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Необходимо ввести данные." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 поле требует вашего внимания." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ полей требуют вашего внимания.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Выбрать --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Элементы с повторным запросом мастер-пароля не могут быть автоматически заполнены при загрузке страницы. Автозаполнение при загрузке страницы выключено.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "Автозаполнение при загрузке страницы использует настройку по умолчанию.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Для редактирования этого поля отключите повторный запрос мастер-пароля", @@ -2911,10 +3240,18 @@ "message": "Разблокируйте ваш аккаунт для просмотра подходящих логинов", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Разблокируйте ваш аккаунт для просмотра предложений по автозаполнению", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Разблокировать аккаунт", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Разблокируйте ваш аккаунт, откроется в новом окне", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Заполнить учетные данные", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Добавить новый элемент в хранилище", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "Новый логин", + "description": "Button text to display within inline menu when there are no matching items on a login field" + }, + "addNewLoginItemAria": { + "message": "Добавление нового логина в хранилище, откроется в новом окне", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "Новая карта", + "description": "Button text to display within inline menu when there are no matching items on a credit card field" + }, + "addNewCardItemAria": { + "message": "Добавление новой карты в хранилище, откроется в новом окне", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "Новая личность", + "description": "Button text to display within inline menu when there are no matching items on an identity field" + }, + "addNewIdentityItemAria": { + "message": "Добавление новой личности в хранилище, откроется в новом окне", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Доступно меню автозаполнения Bitwarden. Для выбора нажмите клавишу со стрелкой вниз.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Ошибка при подключении к сервису Duo. Используйте другой метод двухэтапной аутентификации или обратитесь за помощью в Duo." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Запустите Duo и следуйте шагам для завершения авторизации." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Неверный пароль к файлу. Используйте пароль, введенный при создании файла экспорта." }, - "importDestination": { - "message": "Цель импорта" + "destination": { + "message": "Назначение" }, "learnAboutImportOptions": { "message": "Узнайте о возможностях импорта" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Необходима верификация со стороны инициирующего сайта. Для аккаунтов без мастер-пароля эта возможность пока не реализована." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Войти с passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "У вас нет подходящего логина для этого сайта." }, + "noMatchingLoginsForSite": { + "message": "Нет подходящих логинов для этого сайта" + }, "confirm": { "message": "Подтвердить" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Сохранить passkey как новый логин" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Выберите логин, для которого будет сохранен данный passkey" }, + "chooseCipherForPasskeyAuth": { + "message": "Выберите passkey для авторизации" + }, "passkeyItem": { "message": "Элемент passkey" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Основные форматы", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Перейти к настройкам браузера?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Перейти в справочный центр?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" + }, + "confirmContinueToHelpCenterPasswordManagementContent": { + "message": "Измените настройки автозаполнения и управления паролями в своем браузере.", + "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": "Вы можете просматривать и устанавливать ярлыки расширений в настройках браузера.", + "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": "Измените настройки автозаполнения и управления паролями в своем браузере.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" + }, + "confirmContinueToBrowserKeyboardShortcutSettingsContent": { + "message": "Вы можете просматривать и устанавливать ярлыки расширений в настройках браузера.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Сделать Bitwarden менеджером паролей по умолчанию?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Отключение этого параметра может привести к конфликту между меню автозаполнения Bitwarden и вашим браузером.", + "message": "Игнорирование этого параметра может привести к конфликту между автозаполнениями Bitwarden и вашего браузера.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Учетные данные успешно сохранены!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Пароль сохранен!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Учетные данные успешно обновлены!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Пароль обновлен!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Ошибка сохранения учетных данных. Проверьте консоль для получения подробной информации.", "description": "Notification message for when saving credentials has failed." @@ -3334,20 +3736,6 @@ "passkeyRemoved": { "message": "Passkey удален" }, - "unassignedItemsBannerNotice": { - "message": "Уведомление: Неприсвоенные элементы организации больше не видны в представлении \"Все хранилища\" и доступны только через консоль администратора." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Уведомление: с 16 мая 2024 года не назначенные элементы организации больше не будут видны в представлении \"Все хранилища\" и будут доступны только через консоль администратора." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Назначьте эти элементы в коллекцию из", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "чтобы сделать их видимыми.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Предложения по автозаполнению" }, @@ -3415,7 +3803,7 @@ }, "autofillTitle": { "message": "Автозаполнение - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "Нет значений для копирования" }, - "assignCollections": { - "message": "Назначить коллекции" + "assignToCollections": { + "message": "Назначить коллекциям" }, "copyEmail": { "message": "Скопировать email" @@ -3493,13 +3881,13 @@ "message": "Элементы без папки" }, "itemDetails": { - "message": "Item details" + "message": "Информация об элементе" }, "itemName": { - "message": "Item name" + "message": "Название элемента" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Вы не можете удалить коллекции с правами только на просмотр: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3899,33 @@ "message": "Организация деактивирована" }, "owner": { - "message": "Owner" + "message": "Владелец" }, "selfOwnershipLabel": { - "message": "You", + "message": "Вы", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Доступ к элементам в деактивированных организациях невозможен. Обратитесь за помощью к владельцу организации." }, + "additionalInformation": { + "message": "Дополнительная информация" + }, + "itemHistory": { + "message": "История элемента" + }, + "lastEdited": { + "message": "Последнее изменение" + }, + "ownerYou": { + "message": "Владелец: вы" + }, + "linked": { + "message": "Связано" + }, + "copySuccessful": { + "message": "Скопировано успешно" + }, "upload": { "message": "Загрузить" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Фильтры" + }, + "personalDetails": { + "message": "Личные данные" + }, + "identification": { + "message": "Идентификация" + }, + "contactInfo": { + "message": "Контактная информация" + }, + "downloadAttachment": { + "message": "Скачать - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "номер карты заканчивается на", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Данные для авторизации" + }, + "authenticatorKey": { + "message": "Ключ аутентификатора" + }, + "autofillOptions": { + "message": "Параметры автозаполнения" + }, + "websiteUri": { + "message": "Сайт (URI)" + }, + "websiteUriCount": { + "message": "Веб-сайт (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": "Сайт добавлен" + }, + "addWebsite": { + "message": "Добавить сайт" + }, + "deleteWebsite": { + "message": "Удалить сайт" + }, + "defaultLabel": { + "message": "По умолчанию ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Показать обнаружение совпадений $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Скрыть обнаружение совпадений $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Автозаполнение при загрузке страницы?" + }, + "cardExpiredTitle": { + "message": "Истек срок действия карты" + }, + "cardExpiredMessage": { + "message": "Если вы заменили карту, обновите информацию о ней" + }, + "cardDetails": { + "message": "Реквизиты карты" + }, + "cardBrandDetails": { + "message": "Реквизиты $BRAND$", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Включить анимацию" + }, + "addAccount": { + "message": "Добавить аккаунт" + }, + "loading": { + "message": "Загрузка" + }, + "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": "Назначить" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Только члены организации, имеющие доступ к этим коллекциям, смогут видеть элементы." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Только члены организации, имеющие доступ к этим коллекциям, смогут видеть элементы." + }, + "bulkCollectionAssignmentWarning": { + "message": "Вы выбрали $TOTAL_COUNT$ элемента(-ов). Вы не можете обновить $READONLY_COUNT$ элемента(-ов), поскольку у вас нет прав на редактирование.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Добавить поле" + }, + "add": { + "message": "Добавить" + }, + "fieldType": { + "message": "Тип поля" + }, + "fieldLabel": { + "message": "Метка поля" + }, + "textHelpText": { + "message": "Используйте текстовые поля для простых данных, таких как контрольные вопросы" + }, + "hiddenHelpText": { + "message": "Используйте скрытые поля для конфиденциальных данных, таких как пароли" + }, + "checkBoxHelpText": { + "message": "Используйте флажки, если вы хотите автоматически заполнить поле формы, например, email" + }, + "linkedHelpText": { + "message": "Используйте связанное поле, если у вас возникли проблемы с автозаполнением для конкретного сайта." + }, + "linkedLabelHelpText": { + "message": "Введите HTML-идентификатор поля, имя, aria-label, или плейсхолдер." + }, + "editField": { + "message": "Изменить поле" + }, + "editFieldLabel": { + "message": "Изменить $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Удалить $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ добавлен(о)", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Изменить порядок $LABEL$. Используйте клавиши курсора для перемещения элемента вверх или вниз.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ перемещено вверх, позиция $INDEX$ $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Выбрать коллекции для назначения" + }, + "personalItemTransferWarningSingular": { + "message": "1 элемент будет навсегда передан выбранной организации. Вы больше не будете владельцем этих элементов." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ элементов будут навсегда переданы выбранной организации. Вы больше не будете владельцем этих элементов.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 элемент будет навсегда передан $ORG$. Вы больше не будете владельцем этих элементов.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ элементов будут навсегда переданы $ORG$. Вы больше не будете владельцем этих элементов.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Коллекции успешно назначены" + }, + "nothingSelected": { + "message": "Вы ничего не выбрали." + }, + "movedItemsToOrg": { + "message": "Выбранные элементы перемещены в $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Элементы перемещены в $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Элемент перемещен в $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ перемещено вниз, позиция $INDEX$ $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Расположение элемента" + }, + "fileSends": { + "message": "Файловая Send" + }, + "textSends": { + "message": "Текстовая Send" + }, + "bitwardenNewLook": { + "message": "У Bitwarden новый облик!" + }, + "bitwardenNewLookDesc": { + "message": "Теперь автозаполнение и поиск на вкладке Хранилище стали проще и интуитивно понятнее, чем когда-либо. Осмотритесь!" + }, + "accountActions": { + "message": "Действия с аккаунтом" + }, + "showNumberOfAutofillSuggestions": { + "message": "Показывать количество вариантов автозаполнения логина на значке расширения" + }, + "systemDefault": { + "message": "Системный" + }, + "enterprisePolicyRequirementsApplied": { + "message": "К этой настройке были применены требования корпоративной политики" + }, + "fileSavedToDevice": { + "message": "Файл сохранен на устройстве. Управляйте им из загрузок устройства." + }, + "showCharacterCount": { + "message": "Показать количество символов" + }, + "hideCharacterCount": { + "message": "Скрыть количество символов" + }, + "itemsInTrash": { + "message": "Элементы в корзине" + }, + "noItemsInTrash": { + "message": "Нет элементов в корзине" + }, + "noItemsInTrashDesc": { + "message": "Элементы, которые вы удаляете, появятся здесь и будут удалены навсегда через 30 дней" + }, + "trashWarning": { + "message": "Элементы, которые были в корзине более 30 дней, будут автоматически удалены" + }, + "restore": { + "message": "Восстановить" + }, + "deleteForever": { + "message": "Удалить навсегда" + }, + "noEditPermissions": { + "message": "У вас нет разрешения на редактирование этого элемента" } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index e79a4a4e410..3b47e77a5bd 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "ඔබගේ ආරක්ෂිත සුරක්ෂිතාගාරය වෙත පිවිසීමට හෝ නව ගිණුමක් නිර්මාණය කරන්න." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "ගිණුමක් සාදන්න" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "පිවිසෙන්න" - }, "enterpriseSingleSignOn": { "message": "ව්යවසාය තනි සංඥා මත" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "ප්රධාන මුරපදය ඉඟියක් (විකල්ප)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "ටැබ්" }, @@ -107,17 +113,32 @@ "copySecurityCode": { "message": "ආරක්ෂක කේතය පිටපත් කරන්න" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "ස්වයං-පිරවීම" }, "autoFillLogin": { - "message": "Auto-fill login" + "message": "Autofill login" }, "autoFillCard": { - "message": "Auto-fill card" + "message": "Autofill card" }, "autoFillIdentity": { - "message": "Auto-fill identity" + "message": "Autofill identity" }, "generatePasswordCopied": { "message": "මුරපදය ජනනය (පිටපත්)" @@ -280,6 +301,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,16 +384,56 @@ "message": "Minimum password length" }, "uppercase": { - "message": "Uppercase (A-Z)" + "message": "Uppercase (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Lowercase (a-z)" + "message": "Lowercase (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numbers (0-9)" + "message": "Numbers (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Special characters (!@#$%^&*)" + "message": "Special characters (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "වචන ගණන" @@ -376,7 +455,12 @@ "message": "අවම විශේෂ" }, "avoidAmbChar": { - "message": "අපැහැදිලි චරිත වලින් වළකින්න" + "message": "අපැහැදිලි චරිත වලින් වළකින්න", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "සුරක්ෂිතාගාරය සොයන්න" @@ -556,6 +640,18 @@ "security": { "message": "ආරක්ෂාව" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "දෝෂයක් සිදුවී ඇත" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "ඔබගේ නව ගිණුම නිර්මාණය කර ඇත! ඔබට දැන් ලොග් විය හැකිය." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "සත්යාපන කේතය අවශ්ය වේ." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "වලංගු නොවන සත්යාපන කේතය" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "ඔබගේ පිවිසුම් සැසිය කල් ඉකුත් වී ඇත." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "ඔබට ලොග් වීමට අවශ්ය බව ඔබට විශ්වාසද?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "නව වර්ගය" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "එකතු කරන ලද අයිතමය" }, @@ -737,23 +873,32 @@ "enableAddLoginNotification": { "message": "Ask to add login" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "මෙම “ලොගින් වන්න නිවේදනය එකතු කරන්න” ස්වයංක්රීයව ඔබ පළමු වරට ඔවුන් තුලට ප්රවිෂ්ට සෑම අවස්ථාවකදීම ඔබගේ සුරක්ෂිතාගාරය නව පිවිසුම් බේරා ගැනීමට ඔබෙන් විමසනු." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Show cards on Tab page" }, "showCardsCurrentTabDesc": { - "message": "List card items on the Tab page for easy auto-fill." + "message": "List card items on the Tab page for easy autofill." + }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" }, "showIdentitiesCurrentTabDesc": { - "message": "List identity items on the Tab page for easy auto-fill." + "message": "List identity items on the Tab page for easy autofill." }, "clearClipboard": { "message": "පසුරු පුවරුවට පැහැදිලි", @@ -791,7 +936,7 @@ "message": "යාවත්කාල" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Unlock" @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "පෙරනිමි URI තරග හඳුනා", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "එවැනි ස්වයංක්රීය-පිරවීම ලෙස ක්රියා සිදු කරන විට URI තරගය හඳුනා පිවිසුම් සඳහා කටයුතු කරන බව පෙරනිමි මාර්ගය තෝරන්න." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "ගොනු ඇමුණුම් සඳහා 1 GB සංකේතාත්මක ගබඩා." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "ඔබට bitwarden.com වෙබ් සුරක්ෂිතාගාරයේ වාරික සාමාජිකත්වය මිලදී ගත හැකිය. ඔබට දැන් වෙබ් අඩවියට පිවිසීමට අවශ්යද?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "ඔබ වාරික සාමාජිකයෙක්!" }, "premiumCurrentMemberThanks": { "message": "බිට්වර්ඩන්ට සහාය වීම ගැන ස්තූතියි." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "සියල්ල $PRICE$ /අවුරුද්ද සඳහා!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "සම්පූර්ණ නැවුම් කරන්න" }, @@ -1178,14 +1341,23 @@ "message": "පරිසර URL සුරකිනු ඇත." }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,20 +1371,39 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "පිටු පැටවුම් මත ස්වයංක්රීය-පිරවීම සක්රීය" }, "enableAutoFillOnPageLoadDesc": { "message": "පිවිසුම් පෝරමයක් අනාවරණය කර ඇත්නම්, වෙබ් පිටුව පැටවුම් කරන විට ස්වයංක්රීයව ස්වයංක්රීයව පිරවීම සිදු කරන්න." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Compromised or untrusted websites can exploit autofill on page load." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" }, "learnMoreAboutAutofill": { - "message": "Learn more about auto-fill" + "message": "Learn more about autofill" }, "defaultAutoFillOnPageLoad": { "message": "පිවිසුම් අයිතම සඳහා පෙරනිමි autofill සැකසුම" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "පැති තීරුවේ විවෘත සුරක්ෂිතාගාරය" }, - "commandAutofillDesc": { - "message": "වත්මන් වෙබ් අඩවිය සඳහා අවසන් වරට භාවිතා කරන ලද පිවිසුම ස්වයංක්රීය-පුරවන්න" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "පසුරු පුවරුවට නව අහඹු මුරපදයක් ජනනය කර පිටපත් කරන්න" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "බූලියන්" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "සම්බන්ධිත", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "මුරපද ඉතිහාසය" }, @@ -1533,6 +1742,10 @@ "message": "මූලික වසම", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domain name", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "තරගය හඳුනා", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "පෙරනිමි තරගය හඳුනා", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "ටොගල් කරන්න විකල්ප" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "ලැයිස්තු ගත කිරීමට මුරපද නොමැත." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "ඉවත් කරන්න" }, @@ -1677,6 +1899,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": "සුරක්ෂිතාගාරය කාලය ක්රියාකාරී" }, @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "ඔබගේ නව ප්රධාන මුරපදය ප්රතිපත්ති අවශ්යතා සපුරාලන්නේ නැත." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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": "ජීව විද්යාව සක්රීය කර නැත" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "බැහැර වසම්" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ වලංගු වසමක් නොවේ", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "යවන්න", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "මුරපදය ආරක්ෂා" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "සබැඳිය යවන්න පිටපත් කරන්න", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "නිර්මාණය යවන්න", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "සංස්කරණය යවන්න", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "ඊමේල් සත්යාපනය අවශ්ය වේ" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "මෙම අංගය භාවිතා කිරීම සඳහා ඔබේ විද්යුත් තැපෑල සත්යාපනය කළ යුතුය. වෙබ් සුරක්ෂිතාගාරයේ ඔබගේ විද්යුත් තැපෑල සත්යාපනය කළ හැකිය." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "ස්වයංක්රීය බඳවා ගැනීම" }, @@ -2606,10 +2906,10 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { - "message": "How to auto-fill" + "message": "How to autofill" }, "autofillSelectInfoWithCommand": { "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", @@ -2627,16 +2927,25 @@ "message": "Got it" }, "autofillSettings": { - "message": "Auto-fill settings" + "message": "Autofill settings" + }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index f991cb5624c..c3135bb9fec 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -3,16 +3,19 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "Bitwarden – Bezplatný správca hesiel", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "message": "Bitwarden zabezpečí všetky vaše heslá, prístupové kľúče a citlivé informácie doma, v práci alebo na cestách", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Prihláste sa, alebo vytvorte nový účet pre prístup k vášmu bezpečnému trezoru." }, + "inviteAccepted": { + "message": "Pozvánka prijatá" + }, "createAccount": { "message": "Vytvoriť účet" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Zdajte heslo na vytvorenie účtu" }, - "login": { - "message": "Prihlásiť sa" - }, "enterpriseSingleSignOn": { "message": "Jednotné prihlásenie pre podniky (SSO)" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Nápoveda k hlavnému heslu (voliteľné)" }, + "joinOrganization": { + "message": "Pripojte sa k organizácii" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Dokončite pripojenie k tejto organizácii nastavením hlavného hesla." + }, "tab": { "message": "Karta" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Kopírovať bezpečnostný kód" }, + "copyName": { + "message": "Kopírovať názov" + }, + "copyCompany": { + "message": "Kopírovať spoločnosť" + }, + "copySSN": { + "message": "Kopírovať číslo poistenca sociálnej poisťovne" + }, + "copyPassportNumber": { + "message": "Kopírovať číslo pasu" + }, + "copyLicenseNumber": { + "message": "Kopírovať číslo licencie" + }, "autoFill": { "message": "Automatické vypĺňanie" }, @@ -126,7 +147,7 @@ "message": "Kopírovať názov vlastného poľa" }, "noMatchingLogins": { - "message": "Žiadne zodpovedajúce prihlasovacie údaje." + "message": "Žiadne zodpovedajúce prihlasovacie údaje" }, "noCards": { "message": "Žiadne karty" @@ -150,7 +171,7 @@ "message": "Prihláste sa do trezora" }, "autoFillInfo": { - "message": "Nie sú prístupné žiadne prihlasovacie údaje na automatické vyplnenie pre aktuálnu kartu." + "message": "Nie sú k dispozícii žiadne prihlasovacie údaje na automatické vyplnenie pre aktuálnu kartu." }, "addLogin": { "message": "Pridať prihlasovacie údaje" @@ -159,13 +180,13 @@ "message": "Pridať položku" }, "passwordHint": { - "message": "Nápoveda k heslu" + "message": "Pomôcka pre heslo" }, "enterEmailToGetHint": { "message": "Zadajte emailovú adresu na zaslanie nápovedy pre vaše hlavné heslo." }, "getMasterPasswordHint": { - "message": "Získať nápovedu k hlavnému heslu" + "message": "Získať pomôcku pre hlavné heslo" }, "continue": { "message": "Pokračovať" @@ -242,7 +263,7 @@ "message": "Bitwarden Authenticator" }, "continueToAuthenticatorPageDesc": { - "message": "Bitwarden Authenticator umožňuje uložiť overovacie kľúče a generovať kódy TOTP pre dvojstupňoveé overovanie. Viac informácií nájdete na webovej stránke bitwarden.com" + "message": "Bitwarden Authenticator umožňuje uložiť overovacie kľúče a generovať kódy TOTP pre dvojstupňové overovanie. Viac informácií nájdete na webovej stránke bitwarden.com" }, "bitwardenSecretsManager": { "message": "Bitwarden Secrets Manager" @@ -275,11 +296,29 @@ "message": "Pridať priečinok" }, "name": { - "message": "Meno" + "message": "Názov" }, "editFolder": { "message": "Upraviť priečinok" }, + "newFolder": { + "message": "Nový priečinok" + }, + "folderName": { + "message": "Názov priečinka" + }, + "folderHintText": { + "message": "Vnorte priečinok pridaním názvu nadradeného priečinka a znaku \"/\". Príklad: Sociálne siete/Fóra" + }, + "noFoldersAdded": { + "message": "Neboli pridané žiadne priečinky" + }, + "createFoldersToOrganize": { + "message": "Vytvorte priečinky na usporiadanie položiek trezoru" + }, + "deleteFolderPermanently": { + "message": "Naozaj chcete natrvalo odstrániť tento priečinok?" + }, "deleteFolder": { "message": "Odstrániť priečinok" }, @@ -345,16 +384,56 @@ "message": "Minimálna dĺžka hesla" }, "uppercase": { - "message": "Veľké písmená (A-Z)" + "message": "Veľké písmená (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Malé písmená (a-z)" + "message": "Malé písmená (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Čísla (0-9)" + "message": "Čísla (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Špeciálne znaky (!@#$%^&*)" + "message": "Špeciálne znaky (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Zahrnúť", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Zahrnúť veľké písmená", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Zahrnúť malé písmená", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Zahrnúť čísla", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Zahrnúť špeciálne znaky", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Počet slov" @@ -373,10 +452,15 @@ "message": "Minimum číslic" }, "minSpecial": { - "message": "Minimum špec. znakov" + "message": "Minimum špeciálnych znakov" }, "avoidAmbChar": { - "message": "Vyhnúť sa zameniteľným znakom" + "message": "Vyhnúť sa zameniteľným znakom", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Vyhnúť sa zameniteľným znakom", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Prehľadávať trezor" @@ -556,6 +640,18 @@ "security": { "message": "Zabezpečenie" }, + "confirmMasterPassword": { + "message": "Potvrdiť hlavné heslo" + }, + "masterPassword": { + "message": "Hlavné heslo" + }, + "masterPassImportant": { + "message": "Vaše hlavné heslo sa nebude dať obnoviť, ak ho zabudnete!" + }, + "masterPassHintLabel": { + "message": "Nápoveda pre hlavné heslo" + }, "errorOccurred": { "message": "Vyskytla sa chyba" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Váš nový účet bol vytvorený! Teraz sa môžete prihlásiť." }, + "newAccountCreated2": { + "message": "Váš nový účet bol vytvorený!" + }, + "youHaveBeenLoggedIn": { + "message": "Boli ste prihlásený!" + }, "youSuccessfullyLoggedIn": { "message": "Úspešne ste sa prihlásili" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Overovací kód je povinný." }, + "webauthnCancelOrTimeout": { + "message": "Overenie bolo zrušené alebo trvalo príliš dlho. Skúste to znova." + }, "invalidVerificationCode": { "message": "Neplatný verifikačný kód" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Naskenovať QR kód overovateľa z aktuálnej webovej stránky" }, + "totpHelperTitle": { + "message": "Spravte dvojstupňové overenie bezproblémovým" + }, + "totpHelper": { + "message": "Bitwarden umožňuje uložiť a vyplniť kódy dvojstupňového overenia. Skopírujte a vložte kľúč do tohto poľa." + }, + "totpHelperWithCapture": { + "message": "Bitwarden umožňuje uložiť a vyplniť kódy dvojstupňového overenia. Vyberte ikonu fotoaparátu a zosnímajte obrazovku QR kódu overovacej aplikácie tejto webovej stránky alebo skopírujte a vložte kľúč do tohto poľa." + }, + "learnMoreAboutAuthenticators": { + "message": "Viac informácií o overovateľoch" + }, "copyTOTP": { "message": "Kopírovať kľúč overovateľa (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Vaša relácia vypršala." }, + "logIn": { + "message": "Prihlásiť sa" + }, + "restartRegistration": { + "message": "Zopakovať registráciu" + }, + "expiredLink": { + "message": "Platnosť odkazu vypršala" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Prosím zopakujte registráciu alebo sa pokúste prihlásiť." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Možno už máte účet" + }, "logOutConfirmation": { "message": "Naozaj sa chcete odhlásiť?" }, @@ -670,7 +802,7 @@ "message": "Začiatočnícka príručka" }, "gettingStartedTutorialVideo": { - "message": "Sledujte našu začiatočnícku príručku, aby ste sa naučili, ako získať maximum z nášho rozšírenia prehliadača." + "message": "Pozrite našu príručku pre začiatočníkov, v ktorej sa dozviete, ako získať maximum z nášho rozšírenia pre prehliadač." }, "syncingComplete": { "message": "Synchronizácia kompletná" @@ -697,6 +829,10 @@ "newUri": { "message": "Nové URI" }, + "addDomain": { + "message": "Pridať doménu", + "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": { "message": "Pridaná položka" }, @@ -725,7 +861,7 @@ "message": "Prehľadávať priečinok" }, "searchCollection": { - "message": "Vyhľadať zbierku" + "message": "Hľadať v zbierke" }, "searchType": { "message": "Typ vyhľadávania" @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Požiadať o pridanie prihlásenia" }, + "vaultSaveOptionsTitle": { + "message": "Uložiť do nastavení trezora" + }, "addLoginNotificationDesc": { - "message": "Opýtať sa na pridanie prihlasovacích údajov ak ich ešte nemáte v trezore." + "message": "Opýtať sa na pridanie prihlasovacích údajov, ak ich ešte nemáte v trezore." }, "addLoginNotificationDescAlt": { "message": "Požiada o pridanie položky, ak sa v trezore nenachádza. Platí pre všetky prihlásené účty." }, + "showCardsInVaultView": { + "message": "Zobraziť karty ako návrhy automatického vypĺňania v zobrazení trezora" + }, "showCardsCurrentTab": { "message": "Zobraziť karty na stránke \"Aktuálna karta\"" }, "showCardsCurrentTabDesc": { "message": "Zoznam položiek karty na stránke \"Aktuálna karta\" na jednoduché automatické vyplnenie." }, + "showIdentitiesInVaultView": { + "message": "Zobraziť identity ako návrhy automatického vypĺňania v zobrazení trezora" + }, "showIdentitiesCurrentTab": { "message": "Zobraziť identity na stránke \"Aktuálna karta\"" }, @@ -764,7 +909,7 @@ "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "notificationAddDesc": { - "message": "Má si pre vás Bitwarden zapamätať toto heslo?" + "message": "Má si Bitwarden zapamätať toto heslo?" }, "notificationAddSave": { "message": "Uložiť" @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Predvolené mapovanie", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Vyberte si predvolený spôsob mapovania, ktorý bude použitý pre prihlasovacie údaje pri využití funkcí ako je napríklad automatické vypĺňanie hesiel." @@ -953,7 +1098,7 @@ "message": "Súbor" }, "selectFile": { - "message": "Vybrať súbor." + "message": "Vyberte súbor" }, "maxFileSize": { "message": "Maximálna veľkosť súboru je 500 MB." @@ -983,7 +1128,10 @@ "message": "Zaregistrujte sa pre prémiové členstvo a získajte:" }, "ppremiumSignUpStorage": { - "message": "1 GB šifrovaného úložiska." + "message": "1 GB šifrovaného úložiska na prílohy." + }, + "premiumSignUpEmergency": { + "message": "Núdzový prístup" }, "premiumSignUpTwoStepOptions": { "message": "Proprietárne možnosti dvojstupňového prihlásenia ako napríklad YubiKey a Duo." @@ -1004,7 +1152,10 @@ "message": "Zakúpiť Prémiový účet" }, "premiumPurchaseAlert": { - "message": "Svoje prémiové členstvo môžete zakúpiť vo webovom trezore bitwarden.com. Chcete navštíviť túto stránku teraz?" + "message": "Svoje prémiové členstvo si môžete zakúpiť vo webovom trezore bitwarden.com. Chcete navštíviť túto stránku teraz?" + }, + "premiumPurchaseAlertV2": { + "message": "Prémiové členstvo si môžete zakúpiť v nastaveniach svojho účtu vo webovej aplikácii Bitwarden." }, "premiumCurrentMember": { "message": "Ste prémiovým členom!" @@ -1012,6 +1163,9 @@ "premiumCurrentMemberThanks": { "message": "Ďakujeme, že podporujete Bitwarden." }, + "premiumFeatures": { + "message": "Povýšte na premium a získajte:" + }, "premiumPrice": { "message": "Všetko len za %price% /rok!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "Všetko len za $PRICE$ ročne!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Obnova kompletná" }, @@ -1085,7 +1248,7 @@ "message": "Overiť cez WebAuthn" }, "loginUnavailable": { - "message": "Prihlasovací údaj nedostupný" + "message": "Prihlásenie nie je dispozícii" }, "noTwoStepProviders": { "message": "Tento účet má povolené dvojstupňové prihlásenie, ale žiadny z nakonfigurovaných poskytovateľov nie je podporovaný týmto prehliadačom." @@ -1113,7 +1276,7 @@ "message": "Bezpečnostný kľúč Yubico OTP" }, "yubiKeyDesc": { - "message": "Použiť YubiKey pre prístup k vášmu účtu. Pracuje s YubiKey 4, 4 Nano, 4C a s NEO zariadeniami." + "message": "Použiť YubiKey na prístup k vášmu účtu. Pracuje s YubiKey 4, 4 Nano, 4C a s NEO zariadeniami." }, "duoDescV2": { "message": "Zadajte kód vygenerovaný aplikáciou Duo Security.", @@ -1136,7 +1299,7 @@ "message": "Zadajte kód zaslaný na váš e-mail." }, "selfHostedEnvironment": { - "message": "Sebou hosťované prostredie" + "message": "Prostredie s vlastným hostingom" }, "selfHostedEnvironmentFooter": { "message": "Zadajte základnú URL adresu lokálne hosťovanej inštalácie Bitwarden." @@ -1154,7 +1317,7 @@ "message": "Vlastné prostredie" }, "customEnvironmentFooter": { - "message": "Pre pokročilých používateľov. Môžete špecifikovať základnú URL pre každú službu nezávisle." + "message": "Pre pokročilých používateľov. Základnú adresu URL každej služby môžete určiť samostatne." }, "baseUrl": { "message": "URL servera" @@ -1179,9 +1342,18 @@ }, "showAutoFillMenuOnFormFields": { "message": "Zobraziť ponuku automatického vypĺňania na poliach formulára", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Návrhy automatického vypĺňania" + }, + "showInlineMenuLabel": { + "message": "Zobraziť návrhy automatického vypĺňania v poliach formulára" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Zobraziť návrhy, keď je vybratá ikona" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Platí pre všetky prihlásené účty." }, "turnOffBrowserBuiltInPasswordManagerSettings": { @@ -1202,15 +1374,34 @@ "message": "Keď je vybratá ikona automatického vypĺňania", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Povoliť automatické vypĺňanie pri načítaní stránky" + }, "enableAutoFillOnPageLoad": { "message": "Povoliť automatické vypĺňanie pri načítaní stránky" }, "enableAutoFillOnPageLoadDesc": { "message": "Ak je detekovaný prihlasovací formulár, automaticky vykonať vypĺňanie pri načítaní stránky." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Upozornenie:$CLOSETAG$ Kompromitované alebo nedôveryhodné webové stránky môžu využívať automatické vypĺňanie pri načítaní stránky.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Skompromitované alebo nedôveryhodné stránky môžu pri svojom načítaní zneužiť automatické dopĺňanie." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Viac informácií o rizikách" + }, "learnMoreAboutAutofill": { "message": "Dozvedieť sa viac o automatickom dopĺňaní" }, @@ -1238,9 +1429,15 @@ "commandOpenSidebar": { "message": "Otvoriť trezor v bočnom paneli" }, - "commandAutofillDesc": { + "commandAutofillLoginDesc": { "message": "Automaticky vyplniť naposledy použité prihlasovacie údaje pre túto stránku" }, + "commandAutofillCardDesc": { + "message": "Automaticky vyplniť naposledy použitú kartu pre túto stránku" + }, + "commandAutofillIdentityDesc": { + "message": "Automaticky vyplniť naposledy použitú identitu pre túto stránku" + }, "commandGeneratePasswordDesc": { "message": "Vygenerovať a skopírovať nové náhodné heslo do schránky" }, @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Začiarkavacie políčko" + }, "cfTypeLinked": { "message": "Prepojené", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "Zobraziť $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "História hesla" }, @@ -1533,6 +1742,10 @@ "message": "Základná doména", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Základná doména (odporúčané)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Názov domény", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Spôsob mapovania", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Predvolené mapovanie", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Voľby prepínača" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Neboli nájdené žiadne heslá." }, + "clearHistory": { + "message": "Vymazať históriu" + }, + "noPasswordsToShow": { + "message": "Žiadne heslá na zobrazenie" + }, + "noRecentlyGeneratedPassword": { + "message": "V poslednej dobe ste negenerovali žiadne heslá" + }, "remove": { "message": "Odstrániť" }, @@ -1677,6 +1899,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žiť toto používateľské meno" + }, + "securePasswordGenerated": { + "message": "Bezpečné heslo vygenerované! Nezabudnite tiež aktualizovať heslo na stránke." + }, + "useGeneratorHelpTextPartOne": { + "message": "Použite generátor", + "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": "na vytvorenie silného, unikátneho hesla.", + "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" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Vaše nové heslo nespĺňa pravidlá." }, - "receiveMarketingEmails": { - "message": "Dostávať e-maily od Bitwardenu s oznámeniami, radami a možnosťami výskumu." + "receiveMarketingEmailsV2": { + "message": "Dostávajte do schránky rady, oznámenia a príležitosti na výskum od spoločnosti Bitwarden." }, "unsubscribe": { "message": "Odhlásiť sa z odberu" @@ -1857,7 +2102,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" @@ -1874,6 +2119,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á" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Prosím, odomknite tohto používateľa v počítačovej aplikácii a skúste to znova." }, + "biometricsNotAvailableTitle": { + "message": "Odomykanie biometrickými údajmi je nedostupné" + }, + "biometricsNotAvailableDesc": { + "message": "Odomykanie biometrickými údajmi je momentálne nedostupné. Skúste to znova neskôr." + }, "biometricsFailedTitle": { "message": "Biometria zlyhala" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "Zásady organizácie zablokovali importovanie položiek do vášho osobného trezoru." }, + "domainsTitle": { + "message": "Domény", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Vylúčené domény" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden nebude požadovať ukladanie prihlasovacích údajov pre tieto domény pre všetky prihlásené účty. Aby sa zmeny prejavili, musíte stránku obnoviť." }, + "websiteItemLabel": { + "message": "Webstránka $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ nie je platná doména", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Uložené zmeny vylúčenej domény" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Chránené heslom" }, + "copyLink": { + "message": "Kopírovať odkaz" + }, "copySendLink": { "message": "Kopírovať odkaz na Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send vytvorený", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send bol úspešne vytvorený!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "Send bude k dispozícii každému s odkazom po dobu $DAYS$ dní.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Skopírovaný odkaz na Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send upravený", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Vyžaduje sa overenie e-mailu" }, + "emailVerifiedV2": { + "message": "Overený e-mail" + }, "emailVerificationRequiredDesc": { "message": "Na použitie tejto funkcie musíte overiť svoj e-mail. Svoj e-mail môžete overiť vo webovom trezore." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatická registrácia" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Nastavenia automatického vypĺňania" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Skratka automatického vypĺňania" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Zmeniť skratku" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Spravovať skratky" + }, "autofillShortcut": { "message": "Klávesová skratka automatického vypĺňania" }, - "autofillShortcutNotSet": { - "message": "Skratka automatického vypĺňania nie je nastavená. Zmeníte ju v nastaveniach prehliadača." + "autofillLoginShortcutNotSet": { + "message": "Skratka automatického vypĺňania prihlasovacích údajov nie je nastavená. Zmeníte ju v nastaveniach prehliadača." }, - "autofillShortcutText": { - "message": "Skratka automatického vypĺňania je: $COMMAND$. Zmeníte ju v nastaveniach prehliadača.", + "autofillLoginShortcutText": { + "message": "Skratka automatického vypĺňania prihlasovacích údajov je: $COMMAND$. Zmeníte ju v nastaveniach prehliadača.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Dôveryhodné zariadenie" }, + "sendsNoItemsTitle": { + "message": "Žiadne aktívne Sendy", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Použite Send na bezpečné zdieľanie zašifrovaných informácii s kýmkoľvek.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Vstup je povinný." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "Jedno pole vyžaduje vašu pozornosť." + }, + "multipleFieldsNeedAttention": { + "message": "Niektoré polia ($COUNT$) vyžadujú vašu pozornosť.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Vyberte --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Položky, ktoré vyžadujú opätovné zadanie hlavného hesla sa nedajú automaticky vyplniť pri načítaní stránky. Automatické vypĺňanie pri načítaní stránky je vypnuté.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "Automatické vypĺňanie pri načítaní stránky nastavené na pôvodnú predvoľbu.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Vypnite výzvu na opätovné zadanie hlavného hesla na úpravu tohto poľa", @@ -2911,10 +3240,18 @@ "message": "Odomknite svoj účet na zobrazenie zodpovedajúcich prihlasovacích údajov", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Odomknite svoj účet na zobrazenie návrhov automatického vypĺňania", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Odomknúť účet", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Odomknúť konto v novom okne", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Vyplňte prihlasovacie údaje pre", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Pridať novú položku trezoru", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "Nové prihlasovacie údaje", + "description": "Button text to display within inline menu when there are no matching items on a login field" + }, + "addNewLoginItemAria": { + "message": "Pridať do trezora nové prihlásenie. Otvorí sa v novom okne", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "Nová karta", + "description": "Button text to display within inline menu when there are no matching items on a credit card field" + }, + "addNewCardItemAria": { + "message": "Pridať do trezora novú kartu. Otvorí sa v novom okne", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "Nová identita", + "description": "Button text to display within inline menu when there are no matching items on an identity field" + }, + "addNewIdentityItemAria": { + "message": "Pridať do trezora novú identitu. Otvorí sa v novom okne", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "K dispozícii je ponuka automatického vyplňovania Bitwardenu. Stlačte tlačidlo so šípkou nadol a vyberte.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Chyba pri pripájaní k službe Duo. Použite inú metódu dvojstupňového prihlásenia alebo kontaktujte Duo a požiadajte o pomoc." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Spustite DUO a postupujte podľa pokynov na dokončenie prihlásenia." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Neplatné heslo súboru, použite heslo, ktoré ste zadali pri vytváraní exportného súboru." }, - "importDestination": { - "message": "Cieľ importu" + "destination": { + "message": "Cieľ" }, "learnAboutImportOptions": { "message": "Zistiť viac o možnostiach importu" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Overenie požadované iniciujúcim webom. Táto funkcia zatiaľ nie je implementovaná pre účty bez hlavného hesla." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Prihlásiť sa s prístupovým kľúčom?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "Pre túto stránku nemáte zodpovedajúce prihlasovacie údaje." }, + "noMatchingLoginsForSite": { + "message": "Pre túto stránku sa nenašli prihlasovacie údaje" + }, "confirm": { "message": "Potvrdiť" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Uložiť prístupový kľúč ako nové prihlasovacie údaje" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Vyberte prihlasovacie údaje, do ktorých chcete uložiť prístupový kľúč" }, + "chooseCipherForPasskeyAuth": { + "message": "Vyberte prístupový kľúč, pomocou ktorého sa chcete prihlásiť" + }, "passkeyItem": { "message": "Položka prístupového kľúča" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,6 +3659,30 @@ "message": "Bežné formáty", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Pokračovať do nastavení prehliadača?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Pokračovať v centre pomoci?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" + }, + "confirmContinueToHelpCenterPasswordManagementContent": { + "message": "Zmeňte nastavenia automatického vypĺňania a správy hesiel vo svojom prehliadači.", + "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": "Skratky rozšírenia môžete zobraziť a nastaviť v nastaveniach prehliadača.", + "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": "Zmeňte nastavenia automatického vypĺňania a správy hesiel vo svojom prehliadači.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" + }, + "confirmContinueToBrowserKeyboardShortcutSettingsContent": { + "message": "Skratky rozšírenia môžete zobraziť a nastaviť v nastaveniach prehliadača.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Nastaviť Bitwarden ako predvoleného správcu hesiel?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" @@ -3317,10 +3711,18 @@ "message": "Prihlasovacie údaje boli úspešne uložené!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Heslo bolo uložené!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Prihlasovacie údaje boli úspešne aktualizované!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Heslo bolo aktualizované!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Chyba pri ukladaní prihlasovacích údajov. Viac informácii nájdete v konzole.", "description": "Notification message for when saving credentials has failed." @@ -3334,20 +3736,6 @@ "passkeyRemoved": { "message": "Prístupový kľúč bol odstránený" }, - "unassignedItemsBannerNotice": { - "message": "Upozornenie: Nepriradené položky organizácie už nie sú viditeľné v zobrazení Všetky trezory a sú prístupné iba cez Správcovskú konzolu." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Upozornenie: 16. mája 2024 nepriradené položky organizácie už nebudú viditeľné v zobrazení Všetky trezory a budú prístupné iba cez Správcovskú konzolu." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Priradiť tieto položky do zbierky zo", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ", aby boli viditeľné.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Návrhy automatického vypĺňania" }, @@ -3415,7 +3803,7 @@ }, "autofillTitle": { "message": "Automatické vyplnenie - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "Nie je čo kopírovať" }, - "assignCollections": { - "message": "Prideliť zbierky" + "assignToCollections": { + "message": "Prideliť k zbierkam" }, "copyEmail": { "message": "Skopírovať e-mail" @@ -3493,13 +3881,13 @@ "message": "Položky bez priečinka" }, "itemDetails": { - "message": "Item details" + "message": "Podrobnosti o položke" }, "itemName": { - "message": "Item name" + "message": "Názov položky" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Zbierky, ktoré môžete len zobraziť nemôžete odstrániť: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3899,33 @@ "message": "Organizácia je vypnutá" }, "owner": { - "message": "Owner" + "message": "Vlastník" }, "selfOwnershipLabel": { - "message": "You", + "message": "Vy", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "K položkám vo vypnutej organizácii nie je možné pristupovať. Požiadajte o pomoc vlastníka organizácie." }, + "additionalInformation": { + "message": "Ďalšie informácie" + }, + "itemHistory": { + "message": "História položky" + }, + "lastEdited": { + "message": "Posledná úprava" + }, + "ownerYou": { + "message": "Vlastník: Vy" + }, + "linked": { + "message": "Prepojené" + }, + "copySuccessful": { + "message": "Úspešne skopírované" + }, "upload": { "message": "Nahrať" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filtre" + }, + "personalDetails": { + "message": "Osobné údaje" + }, + "identification": { + "message": "Identifikácia" + }, + "contactInfo": { + "message": "Kontaktné informácie" + }, + "downloadAttachment": { + "message": "Stiahnuť – $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "karta končí číslom", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Prihlasovacie údaje" + }, + "authenticatorKey": { + "message": "Kľúč overovacej aplikácie" + }, + "autofillOptions": { + "message": "Možnosti automatického vypĺňania" + }, + "websiteUri": { + "message": "Webová stránka (URI)" + }, + "websiteUriCount": { + "message": "Webová stránka (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": "Webová stránka pridaná" + }, + "addWebsite": { + "message": "Pridať webovú stránku" + }, + "deleteWebsite": { + "message": "Odstrániť webovú stránku" + }, + "defaultLabel": { + "message": "Predvolené ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Zobraziť spôsob mapovania $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Skryť spôsob mapovania $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Automaticky vyplniť pri načítaní stránky?" + }, + "cardExpiredTitle": { + "message": "Exspirovaná karta" + }, + "cardExpiredMessage": { + "message": "Ak ste kartu obnovili, aktualizujte jej údaje" + }, + "cardDetails": { + "message": "Podrobnosti o karte" + }, + "cardBrandDetails": { + "message": "Podrobnosti o $BRAND$", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Povoliť animácie" + }, + "addAccount": { + "message": "Pridať účet" + }, + "loading": { + "message": "Načíta sa" + }, + "data": { + "message": "Údaje" + }, + "passkeys": { + "message": "Prístupové kľúče", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Heslá", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Prihlásiť sa s prístupovým kľúčom", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, + "assign": { + "message": "Priradiť" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Položku si budú môcť pozrieť len členovia organizácie s prístupom k týmto zbierkam." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Položky si budú môcť pozrieť len členovia organizácie s prístupom k týmto zbierkam." + }, + "bulkCollectionAssignmentWarning": { + "message": "Vybrali ste $TOTAL_COUNT$ položky. Nemôžete aktualizovať $READONLY_COUNT$ položky(-iek), pretože nemáte oprávnenie na úpravu.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Pridať pole" + }, + "add": { + "message": "Pridať" + }, + "fieldType": { + "message": "Typ poľa" + }, + "fieldLabel": { + "message": "Názov poľa" + }, + "textHelpText": { + "message": "Textové polia používajte pre také údaje, ako sú bezpečnostné otázky" + }, + "hiddenHelpText": { + "message": "Skryté polia požívajte pre citlivé údaje ako je heslo" + }, + "checkBoxHelpText": { + "message": "Ak chcete automaticky vyplniť začiarkávacie políčko formulára, napríklad zapamätať e-mail, použite začiarkávacie políčka" + }, + "linkedHelpText": { + "message": "Ak máte problémy s automatickým vypĺňaním pre konkrétnu webovú stránku, použite prepojené pole." + }, + "linkedLabelHelpText": { + "message": "Zadajte html atribút poľa id, name, aria-label, alebo placeholder." + }, + "editField": { + "message": "Upraviť pole" + }, + "editFieldLabel": { + "message": "Upraviť $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Odstrániť $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "Pridané $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Zmeniť poradie $LABEL$. Na presun položky hore alebo dole použite klávesy so šípkami.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ presunuté vyššie, pozícia $INDEX$/$LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Vyberte zbierky na priradenie" + }, + "personalItemTransferWarningSingular": { + "message": "Položka sa natrvalo presunie do vybranej organizácie. Už ju nebudete vlastniť." + }, + "personalItemsTransferWarningPlural": { + "message": "Do vybranej organizácie sa natrvalo presunú $PERSONAL_ITEMS_COUNT$. Tieto položky už nebudete vlastniť.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "Do $ORG$ sa natrvalo presunie jedna položka. Položku už nebudete vlastniť.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "Do $ORG$ sa natrvalo presunú $PERSONAL_ITEMS_COUNT$ položky. Tieto položky už nebudete vlastniť.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Zbierky boli úspešne priradené" + }, + "nothingSelected": { + "message": "Nič ste nevybrali." + }, + "movedItemsToOrg": { + "message": "Vybraté položky boli presunuté do $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Položky presunuté do $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Položka presunutá do $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ presunuté nižšie, pozícia $INDEX$/$LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Umiestnenie položky" + }, + "fileSends": { + "message": "Sendy so súborom" + }, + "textSends": { + "message": "Textové Sendy" + }, + "bitwardenNewLook": { + "message": "Bitwarden má nový vzhľad!" + }, + "bitwardenNewLookDesc": { + "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": "Operácie s účtom" + }, + "showNumberOfAutofillSuggestions": { + "message": "Zobraziť počet odporúčaných prihlasovacích údajov na ikone rozšírenia" + }, + "systemDefault": { + "message": "Predvolené systémom" + }, + "enterprisePolicyRequirementsApplied": { + "message": "Na toto nastavenie boli uplatnené požiadavky pravidiel spoločnosti" + }, + "fileSavedToDevice": { + "message": "Súbor sa uložil do zariadenia. Spravujte stiahnuté súbory zo zariadenia." + }, + "showCharacterCount": { + "message": "Zobraziť počítadlo znakov" + }, + "hideCharacterCount": { + "message": "Skryť počítadlo znakov" + }, + "itemsInTrash": { + "message": "Položky v koši" + }, + "noItemsInTrash": { + "message": "V koši nie sú žiadne položky" + }, + "noItemsInTrashDesc": { + "message": "Položky, ktoré odstránite, sa zobrazia tu a po 30 dňoch sa odstránia natrvalo" + }, + "trashWarning": { + "message": "Položky, ktoré boli v koši viac ako 30 dní budú automaticky odstránené" + }, + "restore": { + "message": "Obnoviť" + }, + "deleteForever": { + "message": "Natrvalo odstrániť" + }, + "noEditPermissions": { + "message": "Na úpravu tejto položky nemáte oprávnenie" } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 55ee4e90bc8..a9665d631af 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Prijavite se ali ustvarite nov račun za dostop do svojega varnega trezorja." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "Ustvari račun" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "Prijavi se" - }, "enterpriseSingleSignOn": { "message": "Enkratna podjetniška prijava." }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Namig za glavno geslo (neobvezno)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "Zavihek" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Kopiraj varnostno kodo" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "Samodejno izpolnjevanje" }, @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "Minimum password length" }, "uppercase": { - "message": "Velike črke (A-Z)" + "message": "Velike črke (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Male črke (a-z)" + "message": "Male črke (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Števke (0-9)" + "message": "Števke (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Posebni znaki (!@#$%^&*)" + "message": "Posebni znaki (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Število besed" @@ -376,7 +455,12 @@ "message": "Minimalno posebnih znakov" }, "avoidAmbChar": { - "message": "Izogibaj se dvoumnim znakom" + "message": "Izogibaj se dvoumnim znakom", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Išči v trezorju" @@ -556,6 +640,18 @@ "security": { "message": "Varnost" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "Prišlo je do napake" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Vaš račun je ustvarjen. Lahko se prijavite." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Koda za preverjanje je obvezna." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Neveljavna koda za preverjanje" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Vaša seja je potekla." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Ste prepričani, da se želite odjaviti?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Nov URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "Element dodan" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Predlagaj dodajanje prijave" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "Predlagaj dodajanje novega elementa, če v trezorju ni ustreznega." }, "addLoginNotificationDescAlt": { "message": "Če predmeta ni v trezorju, ga je potrebno dodati. Velja za vse prijavljene račune." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Prikaži kartice na strani Zavihek" }, "showCardsCurrentTabDesc": { "message": "Na strani Zavihek prikaži kartice za lažje samodejno izpoljnjevanje." }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" + }, "showIdentitiesCurrentTab": { "message": "Prikaži identitete na strani Zavihek" }, @@ -791,7 +936,7 @@ "message": "Da, posodobi zdaj" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Unlock" @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Privzet način preverjanja ujemanja URI-ja", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Izberite privzeti način preverjanja ujemanja URI-ja pri samodejnem izpolnjevanju in drugih dejanjih." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB šifriranega prostora za shrambo podatkov." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Premium članstvo lahko kupite na spletnem trezoju bitwarden.com. Želite obiskati spletno stran zdaj?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Ste premium član!" }, "premiumCurrentMemberThanks": { "message": "Hvala, ker podpirate Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "Vse za samo $PRICE$ /leto!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Osveževanje zaključeno" }, @@ -1178,14 +1341,23 @@ "message": "Environment URLs saved" }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,18 +1371,37 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "Samodejno izpolni, ko se stran naloži" }, "enableAutoFillOnPageLoadDesc": { "message": "Če Bitwarden na strani zazna prijavni obrazec, ga samodejno izpolni takoj, ko se stran naloži." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Spletne strani, ki jim ne zaupate ali v katere so vdrli, lahko zlorabijo samodejno izpolnjevanje ob naložitvi strani." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" + }, "learnMoreAboutAutofill": { "message": "Preberite več o samodejnem izpolnjevanju" }, @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Open vault in sidebar" }, - "commandAutofillDesc": { - "message": "Samodejno izpolni s prijavo, ki je bila na tej strani uporabljena zadnja" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Ustvari novo naključno geslo in ga kopiraj v odložišče" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Logična vrednost" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Povezano polje", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Zgodovina gesel" }, @@ -1533,6 +1742,10 @@ "message": "Bazna domena", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Ime domene", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Preverjanje ujemanja", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Privzeto preverjanje ujemanja", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Prikaži/skrij možnosti" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Ni takšnih gesel." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Odstrani" }, @@ -1677,6 +1899,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" }, @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Vaše novo glavno geslo ne ustreza zahtevam." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Izključene domene" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ ni veljavna domena", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Pošiljke", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Password protected" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Kopiraj povezavo pošiljke", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Pošiljka ustvarjena", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Pošiljka shranjena", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Potrebna je potrditev e-naslova" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Za uporabo te funkcionalnosti morate potrditi svoj e-naslov. To lahko storite v spletnem trezorju." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Nastavitve" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" + }, "autofillShortcut": { "message": "Bližnjica za samodejno izpolnjevanje" }, - "autofillShortcutNotSet": { - "message": "Bližnjična tipka za samodejno izpolnjevanje ni nastavljena. Nastavite jo lahko v nastavitvah brskalnika." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "Bližnjična tipka za samodejno izpolnjevanje je $COMMAND$. Spremenite jo lahko v nastavitvah brskalnika.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index e27f86a0828..c9d37830153 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Пријавите се или креирајте нови налог за приступ сефу." }, + "inviteAccepted": { + "message": "Позив прихваћен" + }, "createAccount": { "message": "Креирај налог" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Завршите креирање налога постављањем лозинке" }, - "login": { - "message": "Пријавите се" - }, "enterpriseSingleSignOn": { "message": "Enterprise Једна Пријава" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Савет Главне Лозинке (опционо)" }, + "joinOrganization": { + "message": "Придружи Организацију" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Завршите придруживање овој организацији постављањем главне лозинке." + }, "tab": { "message": "Језичак" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Копирај сигурносни код" }, + "copyName": { + "message": "Копирај име" + }, + "copyCompany": { + "message": "Копирај фирму" + }, + "copySSN": { + "message": "Копирај број социјалног осигурања" + }, + "copyPassportNumber": { + "message": "Копирај број пасоша" + }, + "copyLicenseNumber": { + "message": "Копирај број лиценце" + }, "autoFill": { "message": "Аутоматско допуњавање" }, @@ -280,6 +301,24 @@ "editFolder": { "message": "Уреди фасциклу" }, + "newFolder": { + "message": "Нова фасцикла" + }, + "folderName": { + "message": "Име фасцикле" + }, + "folderHintText": { + "message": "Угнездите фасциклу додавањем имена надређене фасцкле праћеног знаком „/“. Пример: Друштвени/Форуми" + }, + "noFoldersAdded": { + "message": "Нису додате фасцикле" + }, + "createFoldersToOrganize": { + "message": "Креирајте фасцикле да бисте организовали своје ставке у сефу" + }, + "deleteFolderPermanently": { + "message": "Да ли сте сигурни да желите да трајно избришете ову фасциклу?" + }, "deleteFolder": { "message": "Избриши фасциклу" }, @@ -345,16 +384,56 @@ "message": "Минимална дужина лозинке" }, "uppercase": { - "message": "Велика слова (A-Z)" + "message": "Велика слова (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Мала слова (a-z)" + "message": "Мала слова (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Цифре (0-9)" + "message": "Цифре (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Специјална слова (!@#$%^&*)" + "message": "Специјална слова (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Укључити", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Укључити велика слова", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "А-Ш", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Укључити мала слова", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "а-ш", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Укључит ибројеве", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Укључити специјална слова", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Број речи" @@ -376,7 +455,12 @@ "message": "Минимално специјалних знакова" }, "avoidAmbChar": { - "message": "Избегавај двосмислене карактере" + "message": "Избегавај двосмислене карактере", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Избегавај двосмислене карактере", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Претражи сеф" @@ -556,6 +640,18 @@ "security": { "message": "Сигурност" }, + "confirmMasterPassword": { + "message": "Потрдити главну лозинку" + }, + "masterPassword": { + "message": "Главна Лозинка" + }, + "masterPassImportant": { + "message": "Ваша главна лозинка се не може повратити ако је заборавите!" + }, + "masterPassHintLabel": { + "message": "Савет главне лозинке" + }, "errorOccurred": { "message": "Дошло је до грешке!" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Ваш налог је креиран! Сада се можете пријавити." }, + "newAccountCreated2": { + "message": "Ваш нови налог је направљен!" + }, + "youHaveBeenLoggedIn": { + "message": "Пријављени сте!" + }, "youSuccessfullyLoggedIn": { "message": "Успешно сте се пријавили" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Верификациони код је обавезан." }, + "webauthnCancelOrTimeout": { + "message": "Аутентификација је отказана или је трајала предуго. Молим вас, покушајте поново." + }, "invalidVerificationCode": { "message": "Неисправан верификациони код" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Скенирајте QR кôд аутентификатора са тренутне веб странице" }, + "totpHelperTitle": { + "message": "Учините верификацију у 2 корака беспрекорном" + }, + "totpHelper": { + "message": "Bitwarden може да чува и попуњава верификационе кодове у 2 корака. Копирајте и налепите кључ у ово поље." + }, + "totpHelperWithCapture": { + "message": "Bitwarden може да чува и попуњава верификационе кодове у 2 корака. Изаберите икону камере да бисте направили снимак екрана QR кода за аутентификацију ове веб локације или копирајте и налепите кључ у ово поље." + }, + "learnMoreAboutAuthenticators": { + "message": "Сазнајте више о аутентификаторима" + }, "copyTOTP": { "message": "Копирати једнократни кôд (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Ваша сесија је истекла." }, + "logIn": { + "message": "Пријави се" + }, + "restartRegistration": { + "message": "Поново покрените регистрацију" + }, + "expiredLink": { + "message": "Истекла веза" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Поново покрените регистрацију или покушајте да се пријавите." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Можда већ имате налог" + }, "logOutConfirmation": { "message": "Заиста желите да се одјавите?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Нови линк" }, + "addDomain": { + "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": { "message": "Ставка додата" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Питај пре додавања" }, + "vaultSaveOptionsTitle": { + "message": "Опције чувања у сефу" + }, "addLoginNotificationDesc": { "message": "„Нотификације Додај Лозинку“ аутоматски тражи да сачувате нове пријаве у сефу кад год се први пут пријавите на њих." }, "addLoginNotificationDescAlt": { "message": "Затражите да додате ставку ако она није пронађена у вашем сефу. Односи се на све пријављене налоге." }, + "showCardsInVaultView": { + "message": "Прикажите картице као предлоге за ауто-попуњавање у приказу сефа" + }, "showCardsCurrentTab": { "message": "Прикажи кредитне картице на страници картице" }, "showCardsCurrentTabDesc": { "message": "Прикажи ставке кредитних картица на страници картице за лакше аутоматско допуњавање." }, + "showIdentitiesInVaultView": { + "message": "Прикажите идентитете као предлоге за ауто-попуњавање у приказу сефа" + }, "showIdentitiesCurrentTab": { "message": "Прикажи идентитете на страници" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Стандардно налажење УРЛ", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Изаберите подразумевани начин на који се поступа са откривањем УРЛ за пријаве приликом извођења радњи као што је ауто-попуњавање." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1ГБ шифровано складиште за прилоге." }, + "premiumSignUpEmergency": { + "message": "Хитан приступ." + }, "premiumSignUpTwoStepOptions": { "message": "Приоритарне опције пријаве у два корака као што су YubiKey и Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Можете купити премијум претплату на bitwarden.com. Да ли желите да посетите веб сајт сада?" }, + "premiumPurchaseAlertV2": { + "message": "Можете да купите Премиум у подешавањима налога у веб апликацији Bitwarden." + }, "premiumCurrentMember": { "message": "Ви сте премијум члан!" }, "premiumCurrentMemberThanks": { "message": "Хвала Вам за подршку Bitwarden-а." }, + "premiumFeatures": { + "message": "Надоградите на премиум и добијте:" + }, "premiumPrice": { "message": "Све за само $PRICE$ годишње!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "Све то за само $PRICE$ годишње!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Освежавање је завршено" }, @@ -1179,9 +1342,18 @@ }, "showAutoFillMenuOnFormFields": { "message": "Прикажи мени за ауто-попуњавање на пољима обрасца", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Предлог за ауто-попуњавања" + }, + "showInlineMenuLabel": { + "message": "Прикажи предлоге за ауто-попуњавање у пољима обрасца" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Приказати предлоге када је изабрана икона" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Односи се на све пријављене налоге." }, "turnOffBrowserBuiltInPasswordManagerSettings": { @@ -1202,15 +1374,34 @@ "message": "Када је изабрана икона ауто-попуњавања", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Ауто-попуњавање при учитавању странице" + }, "enableAutoFillOnPageLoad": { "message": "Омогући аутоматско попуњавање након учитавања странице" }, "enableAutoFillOnPageLoadDesc": { "message": "Ако се открије образац за пријаву, извршите аутоматско попуњавање када се веб страница учита." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Пажња:$CLOSETAG$ Компромитоване или непоуздане веб локације могу да искористе ауто-попуњавање при учитавању странице.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Компромитоване или непоуздане веб локације могу да искористе ауто-пуњење при учитавању странице." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Сазнајте више о ризицима" + }, "learnMoreAboutAutofill": { "message": "Сазнајте више о ауто-пуњење" }, @@ -1238,9 +1429,15 @@ "commandOpenSidebar": { "message": "Отвори сеф у бочну траку" }, - "commandAutofillDesc": { + "commandAutofillLoginDesc": { "message": "Аутоматско попуњавање последњу коришћену пријаву за тренутну веб страницу" }, + "commandAutofillCardDesc": { + "message": "Аутоматско попуњавање последњу коришћену картицу за тренутну веб страницу" + }, + "commandAutofillIdentityDesc": { + "message": "Аутоматско попуњавање последњи коришћен идентитет за тренутну веб страницу" + }, "commandGeneratePasswordDesc": { "message": "Генеришите и копирајте нову случајну лозинку у привремену меморију" }, @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Булове" }, + "cfTypeCheckbox": { + "message": "Поље за потврду" + }, "cfTypeLinked": { "message": "Повезано", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "Видети $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Историја Лозинке" }, @@ -1533,6 +1742,10 @@ "message": "Главни домен", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Основни домен (препоручено)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Име домена", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Налажење УРЛ", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Стандардно налажење", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Пребацити опције" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Нема лозинки у листи." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Уклони" }, @@ -1677,6 +1899,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": "Акција на тајмаут сефа" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Ваша нова главна лозинка не испуњава захтеве смерница." }, - "receiveMarketingEmails": { - "message": "Добијајте е-пошту од Bitwarden-а за најаве, савете и могућности истраживања." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Одјави се" @@ -1874,6 +2119,12 @@ "nativeMessagingWrongUserTitle": { "message": "Неподударање налога" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Биометријско откључавање није успело. Биометријски тајни кључ није успео да откључа сеф. Покушајте поново да подесите биометрију." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Неподударање биометријског кључа" + }, "biometricsNotEnabledTitle": { "message": "Биометрија није омогућена" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Откључајте овог корисника у десктоп апликацији и покушајте поново." }, + "biometricsNotAvailableTitle": { + "message": "Биометријско откључавање није доступно" + }, + "biometricsNotAvailableDesc": { + "message": "Биометријско откључавање тренутно није доступно. Покушајте поново касније." + }, "biometricsFailedTitle": { "message": "Биометрија није успела" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "Политика организације је блокирала увоз ставки у ваш појединачни сеф." }, + "domainsTitle": { + "message": "Домени", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Изузети домени" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden неће тражити да сачува податке за пријављивање за ове домене за све пријављене налоге. Морате освежити страницу да би промене ступиле на снагу." }, + "websiteItemLabel": { + "message": "Сајт $number$ (УРЛ)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ није важећи домен", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Изузете промене домена су сачуване" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Заштићено лозинком" }, + "copyLink": { + "message": "Копирај везу" + }, "copySendLink": { "message": "Копирај УРЛ „Send“", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Креирано слање", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send је успешно направљен!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send линк је копиран", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Измењено слање", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Потребна је верификација е-поште" }, + "emailVerifiedV2": { + "message": "Имејл верификован" + }, "emailVerificationRequiredDesc": { "message": "Морате да потврдите е-пошту да бисте користили ову функцију. Можете да потврдите е-пошту у веб сефу." }, @@ -2179,6 +2476,9 @@ "updateWeakMasterPasswordWarning": { "message": "Ваша главна лозинка не испуњава једну или више смерница ваше организације. Да бисте приступили сефу, морате одмах да ажурирате главну лозинку. Ако наставите, одјавићете се са ваше тренутне сесије, што захтева да се поново пријавите. Активне сесије на другим уређајима могу да остану активне до један сат." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Ваша организација је онемогућила шифровање поузданог уређаја. Поставите главну лозинку за приступ вашем трезору." + }, "resetPasswordPolicyAutoEnroll": { "message": "Ауто пријављивање" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Подешавања Ауто-пуњења" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Пречица за ауто-попуњавање" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Промени пречицу" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Управљање пречицама" + }, "autofillShortcut": { "message": "Пречице Ауто-пуњења" }, - "autofillShortcutNotSet": { + "autofillLoginShortcutNotSet": { "message": "Пречица за ауто-попуњавање није подешена. Промените ово у подешавањима претраживача." }, - "autofillShortcutText": { - "message": "Пречица ауто-пуњења је: $COMMAND$. Промените ово у подешавањима претраживача.", + "autofillLoginShortcutText": { + "message": "Пречица за пријављивање за аутоматско попуњавање је $COMMAND$. Управљајте свим пречицама у подешавањима претраживача.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Уређај поуздан" }, + "sendsNoItemsTitle": { + "message": "Нема активних Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Употребите Send да безбедно делите шифроване информације са било ким.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Унос је потребан." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 поље захтева вашу пажњу." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Одабрати --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Ставке са упитом за поновно постављање главне лозинке не могу се ауто-попунити при учитавању странице. Ауто-попуњавање при учитавању странице је искључено.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "Ауто-попуњавање при учитавању странице је подешено да користи подразумевано подешавање.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Искључите поновни упит главне лозинке да бисте уредили ово поље", @@ -2911,10 +3240,18 @@ "message": "Откључајте налог да бисте видели подударне пријаве", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Откључајте свој налог да бисте видели предлоге за ауто-попуњавање", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Откључај налог", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Откључајте свој налог, отвара се у новом прозору", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Попунити акредитиве за", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Додајте нову ставку сефу", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "Ново пријављивање", + "description": "Button text to display within inline menu when there are no matching items on a login field" + }, + "addNewLoginItemAria": { + "message": "Додај нову ставку за пријаву у сеф, отвара се у новом прозору", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "Нова картица", + "description": "Button text to display within inline menu when there are no matching items on a credit card field" + }, + "addNewCardItemAria": { + "message": "Додај нову ставку картице сефа, отвара се у новом прозору", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "Нови идентитет", + "description": "Button text to display within inline menu when there are no matching items on an identity field" + }, + "addNewIdentityItemAria": { + "message": "Додај нов идентитет у сеф, отвара се у новом прозору", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Мени ауто-попуњавања Bitwarden-а је доступан. Притисните тастер са стрелицом надоле да бисте изабрали.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Грешка при повезивању са услугом Duo. Користите други метод пријаве у два корака или контактирајте Duo за помоћ." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Покренути DUO и пратите кораке да бисте завршили пријављивање." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Неважећа лозинка за датотеку, користите лозинку коју сте унели када сте креирали датотеку за извоз." }, - "importDestination": { - "message": "Смештај увоза" + "destination": { + "message": "Одредиште" }, "learnAboutImportOptions": { "message": "Сазнајте више о опцијама увоза" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Верификацију захтева сајт који покреће. Ова функција још увек није имплементирана за налоге без главне лозинке." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Пријавите се са приступачним кључем?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "Немате одговарајућу пријаву за овај сајт." }, + "noMatchingLoginsForSite": { + "message": "Нема одговарајућих пријава за овај сајт" + }, "confirm": { "message": "Потврди" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Сачувати приступни кључ као нову пријаву" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Изаберите пријаву да бисте сачували овај приступни кључ" }, + "chooseCipherForPasskeyAuth": { + "message": "Изаберите приступни кључ за пријаву" + }, "passkeyItem": { "message": "Ставка приступачног кључа" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,6 +3659,30 @@ "message": "Уобичајени формати", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Желите ли да наставите на подешавања претраживача?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Настави на помоћни центар?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" + }, + "confirmContinueToHelpCenterPasswordManagementContent": { + "message": "Промените подешавања ауто-попуњавања и управљања лозинкама у прегледачу.", + "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": "Можете да видите и подесите пречице за екстензије у подешавањима прегледача.", + "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": "Промените подешавања ауто-попуњавања и управљања лозинкама у прегледачу.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" + }, + "confirmContinueToBrowserKeyboardShortcutSettingsContent": { + "message": "Можете да видите и подесите пречице за екстензије у подешавањима прегледача.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Урадите да Bitwarden буде ваш подразумевани менаџер лозинки?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" @@ -3317,10 +3711,18 @@ "message": "Акредитиви су успешно сачувани!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Лозинка је сачувана!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Акредитиви су успешно ажурирани!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Лозинка је ажурирана!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Грешка при чувању акредитива. Проверите конзолу за детаље.", "description": "Notification message for when saving credentials has failed." @@ -3334,20 +3736,6 @@ "passkeyRemoved": { "message": "Приступачни кључ је уклоњен" }, - "unassignedItemsBannerNotice": { - "message": "Напомена: Недодељене ставке организације више нису видљиве у приказу Сви сефови и доступне су само преко Админ конзоле." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Напомена: од 16 Маја 2024м недодељене ставке организације више нису видљиве у приказу Сви сефови и доступне су само преко Админ конзоле." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Предлози за ауто-попуњавање" }, @@ -3415,7 +3803,7 @@ }, "autofillTitle": { "message": "Ауто-пуњење - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "Нема вредности за копирање" }, - "assignCollections": { - "message": "Додели колекције" + "assignToCollections": { + "message": "Додели колекцијама" }, "copyEmail": { "message": "Копирај имејл" @@ -3493,13 +3881,13 @@ "message": "Ставке без фасцикле" }, "itemDetails": { - "message": "Item details" + "message": "Детаљи ставке" }, "itemName": { - "message": "Item name" + "message": "Име ставке" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Не можете уклонити колекције са дозволама само за приказ: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3899,33 @@ "message": "Организација је деактивирана" }, "owner": { - "message": "Owner" + "message": "Власник" }, "selfOwnershipLabel": { - "message": "You", + "message": "Ти", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Није могуће приступити ставкама у деактивираним организацијама. Обратите се власнику ваше организације за помоћ." }, + "additionalInformation": { + "message": "Додатне информације" + }, + "itemHistory": { + "message": "Историја предмета" + }, + "lastEdited": { + "message": "Последња измена" + }, + "ownerYou": { + "message": "Власник: Ви" + }, + "linked": { + "message": "Повезано" + }, + "copySuccessful": { + "message": "Копија успешна" + }, "upload": { "message": "Отпреми" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Филтери" + }, + "personalDetails": { + "message": "Личне информације" + }, + "identification": { + "message": "Идентификација" + }, + "contactInfo": { + "message": "Контакт инфо" + }, + "downloadAttachment": { + "message": "Преузми - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "број картице се завршава са", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Акредитиве за пријављивање" + }, + "authenticatorKey": { + "message": "Кључ аутентификатора" + }, + "autofillOptions": { + "message": "Опције Ауто-пуњења" + }, + "websiteUri": { + "message": "Вебсајт (URI)" + }, + "websiteUriCount": { + "message": "Сајт (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": "Вебсајт додат" + }, + "addWebsite": { + "message": "Додај вебсајт" + }, + "deleteWebsite": { + "message": "Обриши вебсајт" + }, + "defaultLabel": { + "message": "Подразумевано ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Прикажи откривање подударања $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Сакриј откривање подударања $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Ауто-попуњавање при учитавању странице?" + }, + "cardExpiredTitle": { + "message": "Картица је истекла" + }, + "cardExpiredMessage": { + "message": "Ако сте је обновили, ажурирајте податке о картици" + }, + "cardDetails": { + "message": "Детаљи картице" + }, + "cardBrandDetails": { + "message": "$BRAND$ детаљи", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Омогући анимације" + }, + "addAccount": { + "message": "Додај налог" + }, + "loading": { + "message": "Учитавање" + }, + "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": "Додели" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Само чланови организације са приступом овим збиркама ће моћи да виде ставку." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Само чланови организације са приступом овим збиркама ће моћи да виде ставке." + }, + "bulkCollectionAssignmentWarning": { + "message": "Одабрали сте $TOTAL_COUNT$ ставки. Не можете да ажурирате $READONLY_COUNT$ од ставки јер немате дозволе за уређивање.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Додај поље" + }, + "add": { + "message": "Додај" + }, + "fieldType": { + "message": "Врста поља" + }, + "fieldLabel": { + "message": "Ознака поља" + }, + "textHelpText": { + "message": "Користите текстуална поља за податке као што су безбедносна питања" + }, + "hiddenHelpText": { + "message": "Користите скривена поља за осетљиве податке као што је лозинка" + }, + "checkBoxHelpText": { + "message": "Користите поља за потврду ако желите да аутоматски попуните поље за потврду обрасца, на пример имејл за памћење" + }, + "linkedHelpText": { + "message": "Користите повезано поље када имате проблема са аутоматским попуњавањем за одређену веб локацију." + }, + "linkedLabelHelpText": { + "message": "Унесите html Ид поља, име, aria-label, или placeholder." + }, + "editField": { + "message": "Уреди поље" + }, + "editFieldLabel": { + "message": "Уреди $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Обриши $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ је додато", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Преместити $LABEL$. Користите тастер са стрелицом да бисте померили ставку.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ премештено на горе, позиција $INDEX$ од $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Изаберите колекције за доделу" + }, + "personalItemTransferWarningSingular": { + "message": "1 ставка биће трајно пребачена у изабрану организацију. Више нећете имати ову ставку." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ ставке биће трајно пребачени у изабрану организацију. Више нећете имати ове ставке.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 ставка биће трајно пребачена у $ORG$. Више нећете имати ову ставку.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ ставке биће трајно пребачени у $ORG$. Више нећете имати ове ставке.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Успешно додељене колекције" + }, + "nothingSelected": { + "message": "Нисте ништа изабрали." + }, + "movedItemsToOrg": { + "message": "Одабране ставке премештене у $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Ставке премештене у $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Ставка премештена у $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ премештено на доле, позиција $INDEX$ од $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Смештај ставке" + }, + "fileSends": { + "message": "Датотека „Send“" + }, + "textSends": { + "message": "Текст „Send“" + }, + "bitwardenNewLook": { + "message": "Bitwarden има нови изглед!" + }, + "bitwardenNewLookDesc": { + "message": "Лакше је и интуитивније него икада да се аутоматски попуњава и тражи са картице Сефа. Проверите!" + }, + "accountActions": { + "message": "Акције везане за налог" + }, + "showNumberOfAutofillSuggestions": { + "message": "Прикажи број предлога за ауто-попуњавање пријаве на икони додатка" + }, + "systemDefault": { + "message": "Системски подразумевано" + }, + "enterprisePolicyRequirementsApplied": { + "message": "Захтеви политике предузећа су примењени на ово подешавање" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Ставке у смећу" + }, + "noItemsInTrash": { + "message": "Нема ставки у смећу" + }, + "noItemsInTrashDesc": { + "message": "Ставке које избришете ће се појавити овде и биће трајно избрисане након 30 дана" + }, + "trashWarning": { + "message": "Ставке које су биле у смећу више од 30 дана биће аутоматски избрисане" + }, + "restore": { + "message": "Поврати" + }, + "deleteForever": { + "message": "Уклонити заувек" + }, + "noEditPermissions": { + "message": "Немате дозволу да уређујете ову ставку" } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index d2d49dfdbab..8f8d34f8bad 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Logga in eller skapa ett nytt konto för att komma åt ditt säkra valv." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "Skapa konto" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "Logga in" - }, "enterpriseSingleSignOn": { "message": "Single Sign-On för företag" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Huvudlösenordsledtråd (valfri)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "Flik" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Kopiera säkerhetskod" }, + "copyName": { + "message": "Kopiera namn" + }, + "copyCompany": { + "message": "Kopiera företag" + }, + "copySSN": { + "message": "Kopiera personnummer" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "Fyll i automatiskt" }, @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "Minsta tillåtna lösenordslängd" }, "uppercase": { - "message": "Versaler (A-Ö)" + "message": "Versaler (A-Ö)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Gemener (a-ö)" + "message": "Gemener (a-ö)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Siffror (0-9)" + "message": "Siffror (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Specialtecken (!@#$%^&*)" + "message": "Specialtecken (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Antal ord" @@ -376,7 +455,12 @@ "message": "Minsta antal speciella tecken" }, "avoidAmbChar": { - "message": "Undvik tvetydiga tecken" + "message": "Undvik tvetydiga tecken", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Sök i valvet" @@ -556,6 +640,18 @@ "security": { "message": "Säkerhet" }, + "confirmMasterPassword": { + "message": "Bekräfta huvudlösenord" + }, + "masterPassword": { + "message": "Huvudlösenord" + }, + "masterPassImportant": { + "message": "Ditt huvudlösenord kan inte återställas om du glömmer det!" + }, + "masterPassHintLabel": { + "message": "Huvudlösenordsledtråd" + }, "errorOccurred": { "message": "Ett fel har uppstått" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Ditt nya konto har skapats! Du kan logga in nu." }, + "newAccountCreated2": { + "message": "Ditt nya konto har skapats!" + }, + "youHaveBeenLoggedIn": { + "message": "Du har blivit inloggad!" + }, "youSuccessfullyLoggedIn": { "message": "Du är nu inloggad" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Verifieringskod krävs." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Ogiltig verifieringskod" }, @@ -613,7 +718,7 @@ } }, "autofillError": { - "message": "Kunde inte automatiskt fylla i det valda objektet på den här webbsidan. Klipp/klistra informationen istället." + "message": "Det gick inte att fylla i det valda objektet automatiskt på den här webbsidan. Kopiera och klistra in informationen istället." }, "totpCaptureError": { "message": "Det går inte att skanna QR-koden från den aktuella webbsidan" @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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": "Kopiera autentiseringsnyckel (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Din inloggningssession har upphört." }, + "logIn": { + "message": "Logga in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Är du säker på att du vill logga ut?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Ny URI" }, + "addDomain": { + "message": "Lägg till domän", + "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": { "message": "Nytt objekt skapat" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Be om att lägga till inloggning" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "Be om att lägga till ett objekt om det inte finns i ditt valv." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Visa kort på fliksida" }, "showCardsCurrentTabDesc": { "message": "Lista kortobjekt på fliksidan för enkel automatisk fyllning." }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" + }, "showIdentitiesCurrentTab": { "message": "Visa identiteter på fliksidan" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Standardmatchning för URI", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Välj standardalternativet för hur matchning av URI är hanterat för inloggningar när du utför operationer såsom automatisk ifyllnad." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB lagring av krypterade filer." }, + "premiumSignUpEmergency": { + "message": "Nödåtkomst" + }, "premiumSignUpTwoStepOptions": { "message": "Premium-alternativ för tvåstegsverifiering, såsom YubiKey och Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Du kan köpa premium-medlemskap i Bitwardens webbvalv. Vill du besöka webbplatsen nu?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Du är en premium-medlem!" }, "premiumCurrentMemberThanks": { "message": "Tack för att du stödjer Bitwarden." }, + "premiumFeatures": { + "message": "Uppgradera till Premium och få:" + }, "premiumPrice": { "message": "Allt för endast $PRICE$/år!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "Allt för endast $PRICE$ per år!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Uppdatering färdig" }, @@ -1179,13 +1342,22 @@ }, "showAutoFillMenuOnFormFields": { "message": "Visa menyn för automatisk ifyllnad på formulärfält", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { - "message": "Gäller för alla inloggade konton." + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { + "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Redigera webbläsarinställningar." @@ -1202,15 +1374,34 @@ "message": "När ikonen för automatisk ifyllnad är vald", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "Aktivera automatisk ifyllnad vid sidhämtning" }, "enableAutoFillOnPageLoadDesc": { "message": "Utför automatisk ifyllnad om ett inloggningsformulär upptäcks när webbsidan laddas." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Komprometterade eller ej betrodda webbplatser kan utnyttja automatisk ifyllnad vid sidladdning." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" + }, "learnMoreAboutAutofill": { "message": "Läs mer om automatisk ifyllnad" }, @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Öppna valvet i sidofältet" }, - "commandAutofillDesc": { - "message": "Fyll automatiskt i den senast använda inloggningen för den aktuella webbsidan" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Skapa och kopiera ett nytt slumpmässigt lösenord till urklipp." @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Kryssruta" + }, "cfTypeLinked": { "message": "Länkat", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "Visa $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Lösenordshistorik" }, @@ -1533,6 +1742,10 @@ "message": "Basdomän", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Basdomän (rekommenderas)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domännamn", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Matchning", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Standardmatchning", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Växla alternativ" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Det finns inga lösenord att lista." }, + "clearHistory": { + "message": "Rensa historik" + }, + "noPasswordsToShow": { + "message": "Inga lösenord att visa" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Ta bort" }, @@ -1677,6 +1899,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" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Ditt nya huvudlösenord uppfyller inte kraven i policyn." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Lås upp den här användaren i skrivbordsprogrammet och försök igen." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometri misslyckades" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domäner", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Exkluderade domäner" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ är inte en giltig domän", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Lösenordsskyddad" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Kopiera Send-länk", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Ny Send har skapats", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send har sparats", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "E-postverifiering krävs" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Du måste verifiera din e-postadress för att använda den här funktionen. Du kan verifiera din e-postadress i webbvalvet." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatiskt deltagande" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Inställningar för automatisk ifyllnad" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Ändra genväg" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Hantera genvägar" + }, "autofillShortcut": { "message": "Tangentbordsgenväg för automatisk ifyllnad" }, - "autofillShortcutNotSet": { - "message": "Genvägen för automatisk ifyllnad är inte satt. Ändra detta i webbläsarens inställningar." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "Genvägen för automatisk ifyllnad är: $COMMAND$. Ändra detta i webbläsarens inställningar.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Enhet betrodd" }, + "sendsNoItemsTitle": { + "message": "Inga aktiva Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Inmatning är obligatoriskt." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Välj --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Objekt med \"Återupprepa huvudlösenord\" kan inte fyllas i automatiskt vid sidladdning. Automatisk ifyllning vid sidladdning avstängd.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "Aktivera automatisk ifyllnad vid sidhämtning sattes till att använda standardinställningen.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Stäng av \"återupprepa huvudlösenord\" för att redigera detta fält", @@ -2911,10 +3240,18 @@ "message": "Lås upp ditt konto för att visa matchande inloggningar", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Lås upp konto", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fyll i uppgifter för", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Lägg till nytt valvobjekt", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Bitwarden automatisk ifyllnadsmeny är tillgänglig. Tryck på nedåtpilen för att välja.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Starta Duo och följ stegen för att slutföra inloggningen." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Ogiltigt fillösenord, använd lösenordet du angav när du skapade exportfilen." }, - "importDestination": { - "message": "Importdestination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Läs mer om dina importalternativ" @@ -3122,8 +3486,8 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verifiering krävs av den initierande webbplatsen. Denna funktion är ännu inte implementerad för konton utan huvudlösenord." }, - "logInWithPasskey": { - "message": "Logga in med lösennyckel?" + "logInWithPasskeyQuestion": { + "message": "Log in with passkey?" }, "passkeyAlreadyExists": { "message": "En lösennyckel finns redan för detta program." @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "Det finns ingen matchande inloggning för denna webbplats." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Bekräfta" }, @@ -3143,8 +3510,11 @@ "savePasskeyNewLogin": { "message": "Spara lösennyckel som ny inloggning" }, - "choosePasskey": { - "message": "Välj en inloggning för att spara denna lösennyckel till" + "chooseCipherForPasskeySave": { + "message": "Choose a login to save this passkey to" + }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" }, "passkeyItem": { "message": "Lösennyckelobjekt" @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Vanliga format", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Fortsätt till Hjälpcenter?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Låt Bitwarden hantera lösenord som standard?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey borttagen" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "för att göra dem synliga.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Ditt valv är tomt" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Tilldela samlingar" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3496,7 +3884,7 @@ "message": "Item details" }, "itemName": { - "message": "Item name" + "message": "Objektnamn" }, "cannotRemoveViewOnlyCollections": { "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", @@ -3511,15 +3899,33 @@ "message": "Organization is deactivated" }, "owner": { - "message": "Owner" + "message": "Ägare" }, "selfOwnershipLabel": { - "message": "You", + "message": "Du", "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." }, + "additionalInformation": { + "message": "Ytterligare information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Ägare: Du" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Ladda upp" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Autentiseringsnyckel" + }, + "autofillOptions": { + "message": "Autofill options" + }, + "websiteUri": { + "message": "Webbplats (URI)" + }, + "websiteUriCount": { + "message": "Webbplats (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" + }, + "addWebsite": { + "message": "Lägg till webbplats" + }, + "deleteWebsite": { + "message": "Radera webbplats" + }, + "defaultLabel": { + "message": "Standard ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Aktivera animationer" + }, + "addAccount": { + "message": "Lägg till konto" + }, + "loading": { + "message": "Laddar" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Lägg till fält" + }, + "add": { + "message": "Lägg till" + }, + "fieldType": { + "message": "Fälttyp" + }, + "fieldLabel": { + "message": "Fältetikett" + }, + "textHelpText": { + "message": "Använd textfält för data som t. ex. säkerhetsfrågor" + }, + "hiddenHelpText": { + "message": "Använd dolda fält för känslig data, som t. ex. ett lösenord" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Redigera fält" + }, + "editFieldLabel": { + "message": "Redigera $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Radera $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden har fått ett nytt utseende!" + }, + "bitwardenNewLookDesc": { + "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" + }, + "accountActions": { + "message": "Kontoåtgärder" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Visa antal tecken" + }, + "hideCharacterCount": { + "message": "Dölj antal tecken" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 95ec45da720..338d70eaf58 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "Create account" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "Log in" - }, "enterpriseSingleSignOn": { "message": "Enterprise single sign-on" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Master password hint (optional)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "Tab" }, @@ -107,17 +113,32 @@ "copySecurityCode": { "message": "Copy security code" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { - "message": "Auto-fill" + "message": "Autofill" }, "autoFillLogin": { - "message": "Auto-fill login" + "message": "Autofill login" }, "autoFillCard": { - "message": "Auto-fill card" + "message": "Autofill card" }, "autoFillIdentity": { - "message": "Auto-fill identity" + "message": "Autofill identity" }, "generatePasswordCopied": { "message": "Generate password (copied)" @@ -150,7 +171,7 @@ "message": "Log in to your vault" }, "autoFillInfo": { - "message": "There are no logins available to auto-fill for the current browser tab." + "message": "There are no logins available to autofill for the current browser tab." }, "addLogin": { "message": "Add a login" @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "Minimum password length" }, "uppercase": { - "message": "Uppercase (A-Z)" + "message": "Uppercase (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Lowercase (a-z)" + "message": "Lowercase (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numbers (0-9)" + "message": "Numbers (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Special characters (!@#$%^&*)" + "message": "Special characters (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Number of words" @@ -376,7 +455,12 @@ "message": "Minimum special" }, "avoidAmbChar": { - "message": "Avoid ambiguous characters" + "message": "Avoid ambiguous characters", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Search vault" @@ -556,6 +640,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -613,7 +718,7 @@ } }, "autofillError": { - "message": "Unable to auto-fill the selected item on this page. Copy and paste the information instead." + "message": "Unable to autofill the selected item on this page. Copy and paste the information instead." }, "totpCaptureError": { "message": "Unable to scan QR code from the current webpage" @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Your login session has expired." }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "New URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "Item added" }, @@ -737,23 +873,32 @@ "enableAddLoginNotification": { "message": "Ask to add login" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "Ask to add an item if one isn't found in your vault." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "Show cards on Tab page" }, "showCardsCurrentTabDesc": { - "message": "List card items on the Tab page for easy auto-fill." + "message": "List card items on the Tab page for easy autofill." + }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "Show identities on Tab page" }, "showIdentitiesCurrentTabDesc": { - "message": "List identity items on the Tab page for easy auto-fill." + "message": "List identity items on the Tab page for easy autofill." }, "clearClipboard": { "message": "Clear clipboard", @@ -791,7 +936,7 @@ "message": "Update" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Unlock" @@ -810,10 +955,10 @@ }, "defaultUriMatchDetection": { "message": "Default URI match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { - "message": "Choose the default way that URI match detection is handled for logins when performing actions such as auto-fill." + "message": "Choose the default way that URI match detection is handled for logins when performing actions such as autofill." }, "theme": { "message": "Theme" @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB encrypted storage for file attachments." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a Premium member!" }, "premiumCurrentMemberThanks": { "message": "Thank you for supporting Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "All for just $PRICE$ /year!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Refresh complete" }, @@ -1028,7 +1191,7 @@ "message": "Copy TOTP automatically" }, "disableAutoTotpCopyDesc": { - "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login." + "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you autofill the login." }, "enableAutoBiometricsPrompt": { "message": "Ask for biometrics on launch" @@ -1178,14 +1341,23 @@ "message": "Environment URLs saved" }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,38 +1371,57 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "enableAutoFillOnPageLoadDesc": { - "message": "If a login form is detected, auto-fill when the web page loads." + "message": "If a login form is detected, autofill when the web page loads." + }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Compromised or untrusted websites can exploit autofill on page load." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" }, "learnMoreAboutAutofill": { - "message": "Learn more about auto-fill" + "message": "Learn more about autofill" }, "defaultAutoFillOnPageLoad": { "message": "Default autofill setting for login items" }, "defaultAutoFillOnPageLoadDesc": { - "message": "You can turn off auto-fill on page load for individual login items from the item's Edit view." + "message": "You can turn off autofill on page load for individual login items from the item's Edit view." }, "itemAutoFillOnPageLoad": { - "message": "Auto-fill on page load (if set up in Options)" + "message": "Autofill on page load (if set up in Options)" }, "autoFillOnPageLoadUseDefault": { "message": "Use default setting" }, "autoFillOnPageLoadYes": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "autoFillOnPageLoadNo": { - "message": "Do not auto-fill on page load" + "message": "Do not autofill on page load" }, "commandOpenPopup": { "message": "Open vault popup" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Open vault in sidebar" }, - "commandAutofillDesc": { - "message": "Auto-fill the last used login for the current website" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Generate and copy a new random password to the clipboard" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Linked", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -1533,6 +1742,10 @@ "message": "Base domain", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Domain name", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Match detection", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Default match detection", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Toggle options" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "There are no passwords to list." }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Remove" }, @@ -1677,6 +1899,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" }, @@ -1716,16 +1961,16 @@ "message": "Timeout action confirmation" }, "autoFillAndSave": { - "message": "Auto-fill and save" + "message": "Autofill and save" }, "fillAndSave": { "message": "Fill and save" }, "autoFillSuccessAndSavedUri": { - "message": "Item auto-filled and URI saved" + "message": "Item autofilled and URI saved" }, "autoFillSuccess": { - "message": "Item auto-filled " + "message": "Item autofilled " }, "insecurePageWarning": { "message": "Warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page." @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ is not a valid domain", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Password protected" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Copy Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send created", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2606,10 +2906,10 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { - "message": "How to auto-fill" + "message": "How to autofill" }, "autofillSelectInfoWithCommand": { "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", @@ -2627,16 +2927,25 @@ "message": "Got it" }, "autofillSettings": { - "message": "Auto-fill settings" + "message": "Autofill settings" + }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index c676c9e8df2..0edcb1c9f81 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "ล็อกอิน หรือ สร้างบัญชีใหม่ เพื่อใช้งานตู้นิรภัยของคุณ" }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "สร้างบัญชี" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "เข้าสู่ระบบ" - }, "enterpriseSingleSignOn": { "message": "Enterprise Single Sign-On" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Master Password Hint (optional)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "แท็บ" }, @@ -107,17 +113,32 @@ "copySecurityCode": { "message": "คัดลอกรหัสรักษาความปลอดภัย" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "กรอกข้อมูลอัตโนมัติ" }, "autoFillLogin": { - "message": "Auto-fill login" + "message": "Autofill login" }, "autoFillCard": { - "message": "Auto-fill card" + "message": "Autofill card" }, "autoFillIdentity": { - "message": "Auto-fill identity" + "message": "Autofill identity" }, "generatePasswordCopied": { "message": "Generate Password (copied)" @@ -280,6 +301,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,16 +384,56 @@ "message": "Minimum password length" }, "uppercase": { - "message": "ตัวพิมพ์ใหญ่ (A-Z)" + "message": "ตัวพิมพ์ใหญ่ (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "ตัวพิมพ์เล็ก (a-z)" + "message": "ตัวพิมพ์เล็ก (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "ตัวเลข (0-9)" + "message": "ตัวเลข (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "อักขระพิเศษ (!@#$%^&*)" + "message": "อักขระพิเศษ (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Number of Words" @@ -376,7 +455,12 @@ "message": "Minimum Special" }, "avoidAmbChar": { - "message": "Avoid Ambiguous Characters" + "message": "Avoid Ambiguous Characters", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "ค้นหาในตู้นิรภัย" @@ -556,6 +640,18 @@ "security": { "message": "ความปลอดภัย" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "พบข้อผิดพลาด" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "บัญชีใหม่ของคุณถูกสร้างขึ้นแล้ว! ตอนนี้คุณสามารถเข้าสู่ระบบ" }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "ต้องระบุโค้ดยืนยัน" }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "โค้ดยืนยันไม่ถูกต้อง" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Scan authenticator QR code from current webpage" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "เซสชันของคุณหมดอายุแล้ว" }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "คุณต้องการล็อกเอาต์ใช่หรือไม่?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "เพิ่ม URI ใหม่" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "เพิ่มรายการแล้ว" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "ถามเพื่อให้เพิ่มการเข้าสู่ระบบ" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "The \"Add Login Notification\" automatically prompts you to save new logins to your vault whenever you log into them for the first time." }, "addLoginNotificationDescAlt": { "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "แสดงการ์ดบนหน้าแท็บ" }, "showCardsCurrentTabDesc": { "message": "บัตรรายการในหน้าแท็บเพื่อให้ป้อนอัตโนมัติได้ง่าย" }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" + }, "showIdentitiesCurrentTab": { "message": "แสดงตัวตนบนหน้าแท็บ" }, @@ -791,7 +936,7 @@ "message": "Yes, Update Now" }, "notificationUnlockDesc": { - "message": "Unlock your Bitwarden vault to complete the auto-fill request." + "message": "Unlock your Bitwarden vault to complete the autofill request." }, "notificationUnlock": { "message": "Unlock" @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "การตรวจจับการจับคู่ URI เริ่มต้น", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "เลือกวิธีเริ่มต้นในการจัดการการตรวจหาการจับคู่ URI สำหรับการเข้าสู่ระบบเมื่อดำเนินการต่างๆ เช่น การป้อนอัตโนมัติ" @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB of encrypted file storage." }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "You can purchase Premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a Premium member!" }, "premiumCurrentMemberThanks": { "message": "Thank you for supporting bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "All for just $PRICE$ /year!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Refresh complete" }, @@ -1028,7 +1191,7 @@ "message": "Copy TOTP automatically" }, "disableAutoTotpCopyDesc": { - "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you auto-fill the login." + "message": "If a login has an authenticator key, copy the TOTP verification code to your clip-board when you autofill the login." }, "enableAutoBiometricsPrompt": { "message": "Ask for biometrics on launch" @@ -1178,14 +1341,23 @@ "message": "Environment URLs saved" }, "showAutoFillMenuOnFormFields": { - "message": "Show auto-fill menu on form fields", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "message": "Show autofill menu on form fields", + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "Turn off your browser’s built in password manager settings to avoid conflicts." + "message": "Turn off your browser's built in password manager settings to avoid conflicts." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "Edit browser settings." @@ -1199,38 +1371,57 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "When auto-fill icon is selected", + "message": "When autofill icon is selected", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "Enable Auto-fill On Page Load." }, "enableAutoFillOnPageLoadDesc": { - "message": "If a login form is detected, auto-fill when the web page loads." + "message": "If a login form is detected, autofill when the web page loads." + }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } }, "experimentalFeature": { - "message": "Compromised or untrusted websites can exploit auto-fill on page load." + "message": "Compromised or untrusted websites can exploit autofill on page load." + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" }, "learnMoreAboutAutofill": { - "message": "Learn more about auto-fill" + "message": "Learn more about autofill" }, "defaultAutoFillOnPageLoad": { "message": "Default autofill setting for login items" }, "defaultAutoFillOnPageLoadDesc": { - "message": "You can turn off auto-fill on page load for individual login items from the item's Edit view." + "message": "You can turn off autofill on page load for individual login items from the item's Edit view." }, "itemAutoFillOnPageLoad": { - "message": "Auto-fill on page load (if set up in Options)" + "message": "Autofill on page load (if set up in Options)" }, "autoFillOnPageLoadUseDefault": { "message": "Use default setting" }, "autoFillOnPageLoadYes": { - "message": "Auto-fill on page load" + "message": "Autofill on page load" }, "autoFillOnPageLoadNo": { - "message": "Do not auto-fill on page load" + "message": "Do not autofill on page load" }, "commandOpenPopup": { "message": "Open vault popup" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "Open vault in sidebar" }, - "commandAutofillDesc": { - "message": "Auto-fill the last used login for the current website." + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Generate and copy a new random password to the clipboard." @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "Linked", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "ประวัติของรหัสผ่าน" }, @@ -1533,6 +1742,10 @@ "message": "โดเมนพื้นฐาน", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "ชื่อโดเมน", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Match Detection", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "การตรวจจับการจับคู่เริ่มต้น", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Toggle Options" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "ไม่มีรหัสผ่านที่จะแสดง" }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "ลบ" }, @@ -1677,6 +1899,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": "การดำเนินการหลังหมดเวลาล็อคตู้เซฟ" }, @@ -1734,7 +1979,7 @@ "message": "Do you still wish to fill this login?" }, "autofillIframeWarning": { - "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to auto-fill anyway, or Cancel to stop." + "message": "The form is hosted by a different domain than the URI of your saved login. Choose OK to autofill anyway, or Cancel to stop." }, "autofillIframeWarningTip": { "message": "To prevent this warning in the future, save this URI, $HOSTNAME$, to your Bitwarden login item for this site.", @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "รหัสผ่านหลักใหม่ของคุณไม่เป็นไปตามข้อกำหนดของนโยบาย" }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "Biometrics failed" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "An organization policy has blocked importing items into your individual vault." }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ is not a valid domain", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Password protected" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "Copy Send link", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send created", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send saved", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2606,10 +2906,10 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organization policies have turned on autofill on page load." }, "howToAutofill": { - "message": "How to auto-fill" + "message": "How to autofill" }, "autofillSelectInfoWithCommand": { "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", @@ -2627,16 +2927,25 @@ "message": "Got it" }, "autofillSettings": { - "message": "Auto-fill settings" + "message": "Autofill settings" + }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" }, "autofillShortcut": { - "message": "Auto-fill keyboard shortcut" + "message": "Autofill keyboard shortcut" }, - "autofillShortcutNotSet": { - "message": "The auto-fill shortcut is not set. Change this in the browser's settings." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "The auto-fill shortcut is: $COMMAND$. Change this in the browser's settings.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "Default auto-fill shortcut: $COMMAND$.", + "message": "Default autofill shortcut: $COMMAND$.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Device trusted" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Input is required." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Select --" }, @@ -2878,12 +3207,12 @@ "message": "Alias domain" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Autofill on page load set to use default setting.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Turn off master password re-prompt to edit this field", @@ -2896,25 +3225,33 @@ "message": "Skip to content" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Bitwarden autofill menu button", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Toggle Bitwarden autofill menu", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Bitwarden autofill menu", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { "message": "Unlock your account to view matching logins", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Unlock account", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Fill credentials for", "description": "Screen reader text for when overlay item is in focused" @@ -2935,8 +3272,32 @@ "message": "Add new vault item", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Bitwarden autofill menu available. Press the down arrow key to select.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "You do not have a matching login for this site." }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "Confirm" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Save passkey as new login" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { "message": "Passkey Item" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 134bb75db82..02f67a672bc 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Güvenli kasanıza ulaşmak için giriş yapın veya yeni bir hesap oluşturun." }, + "inviteAccepted": { + "message": "Davet kabul edildi" + }, "createAccount": { "message": "Hesap oluştur" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Parolanızı belirleyerek hesabınızı oluşturmayı tamamlayın" }, - "login": { - "message": "Giriş yap" - }, "enterpriseSingleSignOn": { "message": "Kurumsal tek oturum açma (SSO)" }, @@ -50,7 +50,7 @@ "message": "Ana parolanızı unutursanız bu ipucuna bakınca size ana parolanızı hatırlatacak bir şey yazabilirsiniz." }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Parolanızı unutursanız parola ipucunuzu e-posta adresinize gönderebiliriz. Maksimum $CURRENT$/$MAXIMUM$ karakter.", "placeholders": { "current": { "content": "$1", @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Ana parola ipucu (isteğe bağlı)" }, + "joinOrganization": { + "message": "Kuruluşa katıl" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Kuruluşa katılmayı tamamlamak için ana parolanızı belirleyin." + }, "tab": { "message": "Sekme" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Güvenlik kodunu kopyala" }, + "copyName": { + "message": "Adı kopyala" + }, + "copyCompany": { + "message": "Şirketi kopyala" + }, + "copySSN": { + "message": "Sosyal güvenlik numarasını kopyala" + }, + "copyPassportNumber": { + "message": "Pasaport numarasını kopyala" + }, + "copyLicenseNumber": { + "message": "Ruhsat numarasını kopyala" + }, "autoFill": { "message": "Otomatik doldur" }, @@ -245,7 +266,7 @@ "message": "Bitwarden Authenticator allows you to store authenticator keys and generate TOTP codes for 2-step verification flows. Learn more on the bitwarden.com website" }, "bitwardenSecretsManager": { - "message": "Bitwarden Sır Yöneticisi" + "message": "Bitwarden Secrets Manager" }, "continueToSecretsManagerPageDesc": { "message": "Securely store, manage, and share developer secrets with Bitwarden Secrets Manager. Learn more on the bitwarden.com website." @@ -280,6 +301,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" }, @@ -345,16 +384,56 @@ "message": "Minimum parola uzunluğu" }, "uppercase": { - "message": "Büyük harf (A-Z)" + "message": "Büyük harf (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Küçük harf (a-z)" + "message": "Küçük harf (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Rakamlar (0-9)" + "message": "Rakamlar (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Özel karakterler (!@#$%^&*)" + "message": "Özel karakterler (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Dahil et", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Büyük harfleri dahil et", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Küçük harfleri dahil et", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Rakamları dahil et", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Özel karakterleri dahil et", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Kelime sayısı" @@ -376,7 +455,12 @@ "message": "En az özel karakter" }, "avoidAmbChar": { - "message": "Okurken karışabilecek karakterleri kullanma" + "message": "Okurken karışabilecek karakterleri kullanma", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Okurken karışabilecek karakterleri kullanma", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Kasada ara" @@ -556,6 +640,18 @@ "security": { "message": "Güvenlik" }, + "confirmMasterPassword": { + "message": "Ana parolayı onaylayın" + }, + "masterPassword": { + "message": "Ana parola" + }, + "masterPassImportant": { + "message": "Ana parolanızı unutursanız kurtaramazsınız!" + }, + "masterPassHintLabel": { + "message": "Ana parola ipucu" + }, "errorOccurred": { "message": "Bir hata oluştu" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Yeni hesabınız oluşturuldu! Şimdi giriş yapabilirsiniz." }, + "newAccountCreated2": { + "message": "Yeni hesabınız oluşturuldu." + }, + "youHaveBeenLoggedIn": { + "message": "Oturum açtınız." + }, "youSuccessfullyLoggedIn": { "message": "Başarıyla giriş yaptınız" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Doğrulama kodu gereklidir." }, + "webauthnCancelOrTimeout": { + "message": "Kimlik doğrulama iptal edildi ve çok uzun sürdü. Lütfen yeniden deneyin." + }, "invalidVerificationCode": { "message": "Geçersiz doğrulama kodu" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Mevcut web sayfasındaki kimlik doğrulayıcı QR kodunu tarayın" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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": "Kimlik doğrulama anahtarını kopyala (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Oturumunuz zaman aşımına uğradı." }, + "logIn": { + "message": "Giriş yap" + }, + "restartRegistration": { + "message": "Kaydı yeniden başlat" + }, + "expiredLink": { + "message": "Bağlantının süresi dolmuş" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Lütfen kaydı yeniden başlatın veya giriş yapmayı deneyin." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Zaten hesabınız olabilir" + }, "logOutConfirmation": { "message": "Çıkış yapmak istediğinize emin misiniz?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "Yeni URI" }, + "addDomain": { + "message": "Alan adı ekle", + "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": { "message": "Kayıt eklendi" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Hesap eklemeyi öner" }, + "vaultSaveOptionsTitle": { + "message": "Kasa seçeneklerine kaydet" + }, "addLoginNotificationDesc": { "message": "\"Hesap ekle\" bildirimi, ilk kez kullandığınız hesap bilgilerini kasanıza kaydetmek isteyip istemediğinizi otomatik olarak sorar." }, "addLoginNotificationDescAlt": { "message": "Kasanızda bulunmayan kayıtların eklenmesini isteyip istemediğinizi sorar. Oturum açmış tüm hesaplar için geçerlidir." }, + "showCardsInVaultView": { + "message": "Kasa görünümünde kartları otomatik doldurma önerisi olarak göster" + }, "showCardsCurrentTab": { "message": "Sekme sayfasında kartları göster" }, "showCardsCurrentTabDesc": { "message": "Kolay otomatik doldurma için sekme sayfasında kart öğelerini listele" }, + "showIdentitiesInVaultView": { + "message": "Kasa görünümünde kimlikleri otomatik doldurma önerisi olarak göster" + }, "showIdentitiesCurrentTab": { "message": "Sekme sayfasında kimlikleri göster" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Varsayılan URI eşleşme tespiti", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Otomatik doldurma gibi eylemler gerçekleştirilirken hesaplar için URI eşleşme tespitinin nasıl yapılacağını seçin." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "Dosya ekleri için 1 GB şifrelenmiş depolama." }, + "premiumSignUpEmergency": { + "message": "Acil durum erişimi" + }, "premiumSignUpTwoStepOptions": { "message": "YubiKey ve Duo gibi marka bazlı iki aşamalı giriş seçenekleri." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Premium üyeliği bitwarden.com web kasası üzerinden satın alabilirsiniz. Şimdi siteye gitmek ister misiniz?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Premium üyesiniz!" }, "premiumCurrentMemberThanks": { "message": "Bitwarden'ı desteklediğiniz için teşekkür ederiz." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "Bunların hepsi sadece yılda $PRICE$!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "Bunların hepsi yılda sadece $PRICE$!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Yenileme tamamlandı" }, @@ -1106,17 +1269,17 @@ "message": "Kimlik doğrulama uygulaması" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Kimlik doğrulama uygulamanızın (örn. Bitwarden Authenticator) ürettiği kodu girin.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP Security Key" + "message": "YubiKey OTP güvenlik anahtarı" }, "yubiKeyDesc": { "message": "Hesabınıza erişmek için bir YubiKey kullanın. YubiKey 4, 4 Nano, 4C ve NEO cihazlarıyla çalışır." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Duo Security'nin ürettiği kodu girin.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1133,7 +1296,7 @@ "message": "E-posta" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "E-posta adresinize gönderilen kodu girin." }, "selfHostedEnvironment": { "message": "Şirket içinde barındırılan ortam" @@ -1179,9 +1342,18 @@ }, "showAutoFillMenuOnFormFields": { "message": "Form alanlarında otomatik doldurma menüsünü göster", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Önerileri otomatik doldur" + }, + "showInlineMenuLabel": { + "message": "Form alanlarında otomatik doldurma önerilerini göster" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Simge seçildiğinde önerileri göster" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Oturum açmış tüm hesaplara uygulanır." }, "turnOffBrowserBuiltInPasswordManagerSettings": { @@ -1202,15 +1374,34 @@ "message": "Otomatik doldurma simgesi seçildiğinde", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Sayfa yüklendiğinde otomatik doldur" + }, "enableAutoFillOnPageLoad": { "message": "Sayfa yüklendiğinde otomatik doldur" }, "enableAutoFillOnPageLoadDesc": { "message": "Sayfa yüklendiğinde giriş formu tespit edilirse otomatik olarak formu doldur." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Uyarı:$CLOSETAG$ Ele geçirilmiş veya güvenilmeyen web siteleri sayfa yüklenirken otomatik doldurmayı suistimal edebilir.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Ele geçirilmiş veya güvenilmeyen web siteleri sayfa yüklenirken otomatik doldurmayı suistimal edebilir." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Riskleri öğrenin" + }, "learnMoreAboutAutofill": { "message": "Otomatik doldurma hakkında bilgi alın" }, @@ -1238,9 +1429,15 @@ "commandOpenSidebar": { "message": "Kasayı kenar çubuğunda aç" }, - "commandAutofillDesc": { + "commandAutofillLoginDesc": { "message": "Geçerli site için son kullanılan hesabı otomatik doldur" }, + "commandAutofillCardDesc": { + "message": "Geçerli site için son kullanılan kartı otomatik doldur" + }, + "commandAutofillIdentityDesc": { + "message": "Geçerli site için son kullanılan kimliği otomatik doldur" + }, "commandGeneratePasswordDesc": { "message": "Rastgele yeni bir parola oluştur ve panoya kopyala" }, @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Boolean" }, + "cfTypeCheckbox": { + "message": "Onay kutusu" + }, "cfTypeLinked": { "message": "Bağlantılı", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "$TYPE$ bilgileri", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Parola geçmişi" }, @@ -1533,6 +1742,10 @@ "message": "Ana alan adı", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Alan adı kökü (önerilen)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Alan adı", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Eşleşme tespiti", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Varsayılan eşleşme tespiti", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Seçenekleri aç/kapat" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Listelenecek parola yok." }, + "clearHistory": { + "message": "Geçmişi temizle" + }, + "noPasswordsToShow": { + "message": "Gösterilecek parola yok" + }, + "noRecentlyGeneratedPassword": { + "message": "Yakın zamanda parola üretmediniz" + }, "remove": { "message": "Kaldır" }, @@ -1677,6 +1899,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": "Güvenli parola üretildi. Web sitesindeki parolanızı da güncellemeyi unutmayın." + }, + "useGeneratorHelpTextPartOne": { + "message": "Güçlü ve benzersiz bir parola üretmek için", + "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": "üreteci kullanabilirsiniz", + "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" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Yeni ana parolanız ilke gereksinimlerini karşılamıyor." }, - "receiveMarketingEmails": { - "message": "Bitwarden'dan duyurular, öneriler ve araştırmalarla ilgili e-postalar alın." + "receiveMarketingEmailsV2": { + "message": "Bitwarden'dan öneriler, duyurular ve araştırma fırsatları e-posta adresinize gelsin." }, "unsubscribe": { "message": "İstediğiniz zaman" @@ -1874,6 +2119,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ış" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Lütfen masaüstü uygulamasından bu kullanıcının kilidini açıp yeniden deneyin." }, + "biometricsNotAvailableTitle": { + "message": "Biyometrik kilit açma kullanılamıyor" + }, + "biometricsNotAvailableDesc": { + "message": "Biyometrik kilit açma şu anda kullanılamıyor. Lütfen daha sonra tekrar deneyin." + }, "biometricsFailedTitle": { "message": "Biyometri doğrulanamadı" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "Bir kuruluş ilkesi, kayıtları kişisel kasanıza içe aktarmayı engelledi." }, + "domainsTitle": { + "message": "Alan adları", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "Hariç tutulan alan adları" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden, oturum açmış tüm hesaplar için bu alan adlarının hesap bilgilerini kaydetmeyi sormayacaktır. Değişikliklerin etkili olması için sayfayı yenilemeniz gerekir." }, + "websiteItemLabel": { + "message": "Web sitesi $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ geçerli bir alan adı değil", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Alan adı istisnası değişiklikleri kaydedildi" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Parola korumalı" }, + "copyLink": { + "message": "Bağlantıyı kopyala" + }, "copySendLink": { "message": "Send bağlantısını kopyala", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send oluşturuldu", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send başarıyla oluşturuldu.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "$DAYS$ gün boyunca bu bağlantıya sahip olan herkes bu Send'e ulaşabilir.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send bağlantısı kopyalandı", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send kaydedildi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "E-posta doğrulaması gerekiyor" }, + "emailVerifiedV2": { + "message": "E-posta doğrulandı" + }, "emailVerificationRequiredDesc": { "message": "Bu özelliği kullanmak için e-postanızı doğrulamanız gerekir. E-postanızı web kasasında doğrulayabilirsiniz." }, @@ -2179,6 +2476,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." + }, "resetPasswordPolicyAutoEnroll": { "message": "Otomatik eklenme" }, @@ -2310,7 +2610,7 @@ "message": "Kuruluş kasasını dışa aktarma" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Yalnızca $ORGANIZATION$ ile ilişkili kuruluş kasası dışarı aktarılacak. Kişisel kasalardaki ve diğer kuruluşlardaki kayıtlar dahil edilmeyecek.", "placeholders": { "organization": { "content": "$1", @@ -2430,7 +2730,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "Geçersiz $SERVICENAME$ alan adı.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -2440,7 +2740,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "Geçersiz $SERVICENAME$ URL'si.", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -2450,7 +2750,7 @@ } }, "forwarderUnknownError": { - "message": "Unknown $SERVICENAME$ error occurred.", + "message": "Bilinmeyen $SERVICENAME$ hatası oluştu.", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -2460,7 +2760,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Unknown forwarder: '$SERVICENAME$'.", + "message": "Bilinmeyen yönlendirici: '$SERVICENAME$'.", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Otomatik doldurma ayarları" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Otomatik doldurma kısayolu" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Kısayolu değiştir" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Kısayolları yönet" + }, "autofillShortcut": { "message": "Otomatik doldurma klavye kısayolu" }, - "autofillShortcutNotSet": { - "message": "Otomatik doldurma kısayolu ayarlanmamış. Bunu tarayıcının ayarlarından değiştirebilirsiniz." + "autofillLoginShortcutNotSet": { + "message": "Hesabı otomatik doldurma kısayolu ayarlanmamış. Bunu tarayıcı ayarlarından değiştirebilirsiniz." }, - "autofillShortcutText": { - "message": "Otomatik doldurma kısayolu: $COMMAND$. Bunu tarayıcının ayarlarından değiştirebilirsiniz.", + "autofillLoginShortcutText": { + "message": "Hesabı otomatik doldurma kısayolu: $COMMAND$. Tüm kısayolları tarayıcı ayarlarından değiştirebilirsiniz.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Cihaza güvenildi" }, + "sendsNoItemsTitle": { + "message": "Aktif Send yok", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Şifrelenmiş bilgileri güvenle paylaşmak için Send'i kullanabilirsiniz.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Girdi gerekli." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 alanla ilgilenmeniz gerekiyor." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ alanla ilgilenmeniz gerekiyor.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Seçin --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Ana parolayı yeniden isteyen kayıtlar sayfa yüklendiğinde otomatik olarak doldurulamaz. Sayfa yüklendiğinde otomatik doldurma kapatıldı.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "Sayfa yüklendiğinde otomatik doldurma, varsayılan ayarı kullanacak şekilde ayarlandı.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Bu alanı düzenlemek için ana parolayı yeniden istemeyi kapatın", @@ -2911,10 +3240,18 @@ "message": "Eşleşen hesaplarınızı görmek için hesabınızın kilidini açın", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Otomatik doldurma önerilerini görmek için hesabınızın kilidini açın", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Hesap kilidini aç", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Bilgileri doldur", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Kasaya yeni kayıt ekle", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "Yeni hesap", + "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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "Yeni kart", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "Yeni kimlik", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Bitwarden otomatik doldurma menüsü mevcut. Seçmek için aşağı ok tuşuna basın.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,8 +3382,11 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Duo'yu başlatın ve oturum açmayı tamamlamak için adımları izleyin." + "message": "Duo'yu açın ve girişi tamamlamak için adımları izleyin." }, "duoRequiredForAccount": { "message": "Hesabınız için Duo iki adımlı giriş gereklidir." @@ -3034,7 +3398,7 @@ "message": "Uzantıyı dışarı al" }, "launchDuo": { - "message": "Duo'yu başlat" + "message": "Duo'yu aç" }, "importFormatError": { "message": "Veriler doğru biçimlendirilmemiş. Lütfen içe aktarma dosyanızı kontrol edin ve tekrar deneyin." @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Geçersiz dosya parolası. Lütfen dışa aktardığınız dosyayı oluştururken girdiğiniz parolayı kullanın." }, - "importDestination": { - "message": "İçe aktarma hedefi" + "destination": { + "message": "Hedef" }, "learnAboutImportOptions": { "message": "İçe aktarma seçeneklerinizi öğrenin" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Site kimlik doğrulaması gerektiriyor. Bu özellik henüz ana parolası olmayan hesaplarda kullanılamaz." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Geçiş anahtarı ile giriş yapılsın mı?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "Bu siteyle eşleşen hiç hesabınız yok." }, + "noMatchingLoginsForSite": { + "message": "Bu siteyle eşleşen hesap bulunamadı" + }, "confirm": { "message": "Onayla" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Geçiş anahtarını yeni hesap olarak kaydet" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Bu geçiş anahtarının kaydedileceği hesabı seçin" }, + "chooseCipherForPasskeyAuth": { + "message": "Giriş yapılacak geçiş anahtarını seçin" + }, "passkeyItem": { "message": "Geçiş anahtarı kaydı" }, @@ -3268,7 +3638,7 @@ "message": "konum" }, "useDeviceOrHardwareKey": { - "message": "Cihazınızı veya donanımsal anahtarınızı kullanın" + "message": "Cihazımı veya anahtar donanımımı kullanacağım" }, "justOnce": { "message": "Yalnızca bir defa" @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,6 +3659,30 @@ "message": "Sık kullanılan biçimler", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Tarayıcı ayarlarına gidilsin mi?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Yardım merkezine gitmek ister misiniz?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Bitwarden varsayılan parola yöneticiniz yapılsın mı?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" @@ -3317,10 +3711,18 @@ "message": "Kimlik bilgileri başarıyla kaydedildi!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Parola kaydedildi!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Kimlik bilgileri başarıyla güncellendi!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Parola güncellendi!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Kimlik bilgileri kaydedilirken hata oluştu. Ayrıntılar için konsolu kontrol edin.", "description": "Notification message for when saving credentials has failed." @@ -3334,20 +3736,6 @@ "passkeyRemoved": { "message": "Geçiş anahtarı kaldırıldı" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Önerileri otomatik doldur" }, @@ -3415,7 +3803,7 @@ }, "autofillTitle": { "message": "Otomatik doldur - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "Kopyalanacak değer yok" }, - "assignCollections": { - "message": "Koleksiyon ata" + "assignToCollections": { + "message": "Koleksiyonlara ata" }, "copyEmail": { "message": "E-postayı kopyala" @@ -3493,10 +3881,10 @@ "message": "Klasörü olmayan kayıtlar" }, "itemDetails": { - "message": "Item details" + "message": "Kayıt ayrıntıları" }, "itemName": { - "message": "Item name" + "message": "Kayıt adı" }, "cannotRemoveViewOnlyCollections": { "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", @@ -3511,26 +3899,44 @@ "message": "Kuruluş pasifleştirilmiş" }, "owner": { - "message": "Owner" + "message": "Sahibi" }, "selfOwnershipLabel": { - "message": "You", + "message": "Siz", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "Pasif kuruluşlardaki kayıtlara erişilemez. Destek almak için kuruluş sahibinizle iletişime geçin." }, + "additionalInformation": { + "message": "Ek bilgiler" + }, + "itemHistory": { + "message": "Öğe geçmişi" + }, + "lastEdited": { + "message": "Son düzenlenme" + }, + "ownerYou": { + "message": "Sahibi: Siz" + }, + "linked": { + "message": "Bağlandı" + }, + "copySuccessful": { + "message": "Kopyalama başarılı" + }, "upload": { - "message": "Upload" + "message": "Yükle" }, "addAttachment": { - "message": "Add attachment" + "message": "Dosya ekle" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "Maksimum dosya boyutu 500 MB'dir" }, "deleteAttachmentName": { - "message": "Delete attachment $NAME$", + "message": "$NAME$ dosyasını sil", "placeholders": { "name": { "content": "$1", @@ -3539,7 +3945,7 @@ } }, "downloadAttachmentName": { - "message": "Download $NAME$", + "message": "$NAME$ dosyasını indir", "placeholders": { "name": { "content": "$1", @@ -3548,7 +3954,7 @@ } }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Are you sure you want to permanently delete this attachment?" + "message": "Bu dosyayı kalıcı olarak silmek istediğinizden emin misiniz?" }, "premium": { "message": "Premium" @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filtreler" + }, + "personalDetails": { + "message": "Kişisel bilgiler" + }, + "identification": { + "message": "Kimlik" + }, + "contactInfo": { + "message": "İletişim bilgileri" + }, + "downloadAttachment": { + "message": "İndir - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "kart numarasının sonu", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Hesap bilgileri" + }, + "authenticatorKey": { + "message": "Kimlik doğrulama anahtarı" + }, + "autofillOptions": { + "message": "Otomatik doldurma ayarları" + }, + "websiteUri": { + "message": "Web sitesi (URI)" + }, + "websiteUriCount": { + "message": "Web sitesi (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": "Web sitesi eklendi" + }, + "addWebsite": { + "message": "Web sitesi ekle" + }, + "deleteWebsite": { + "message": "Web sitesini sil" + }, + "defaultLabel": { + "message": "Varsayılan ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Sayfa yüklendiğinde otomatik doldur" + }, + "cardExpiredTitle": { + "message": "Kartın süresi dolmuş" + }, + "cardExpiredMessage": { + "message": "Kartı yenilediyseniz kart bilgilerini güncelleyin" + }, + "cardDetails": { + "message": "Kart bilgileri" + }, + "cardBrandDetails": { + "message": "$BRAND$ bilgileri", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Animasyonları etkinleştir" + }, + "addAccount": { + "message": "Hesap ekle" + }, + "loading": { + "message": "Yükleniyor" + }, + "data": { + "message": "Veri" + }, + "passkeys": { + "message": "Geçiş Anahtarları", + "description": "A section header for a list of passkeys." + }, + "passwords": { + "message": "Parolalar", + "description": "A section header for a list of passwords." + }, + "logInWithPasskeyAriaLabel": { + "message": "Geçiş anahtarıyla giriş yap", + "description": "ARIA label for the inline menu button that logs in with a passkey." + }, + "assign": { + "message": "Ata" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Alan ekle" + }, + "add": { + "message": "Ekle" + }, + "fieldType": { + "message": "Alan türü" + }, + "fieldLabel": { + "message": "Alan etiketi" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Alanı düzenle" + }, + "editFieldLabel": { + "message": "$LABEL$ alanını düzenle", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ eklendi", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Kayıt konumu" + }, + "fileSends": { + "message": "Dosya Send'leri" + }, + "textSends": { + "message": "Metin Send'leri" + }, + "bitwardenNewLook": { + "message": "Bitwarden'ın tasarımı güncellendi!" + }, + "bitwardenNewLookDesc": { + "message": "Otomatik doldurma ve kasanızda arama yapma artık eskisinden daha kolay. Yeni tasarıma göz atmayı unutmayın!" + }, + "accountActions": { + "message": "Account actions" + }, + "showNumberOfAutofillSuggestions": { + "message": "Otomatik öneri sayısını uzantı simgesinde göster" + }, + "systemDefault": { + "message": "Sistem varsayılanı" + }, + "enterprisePolicyRequirementsApplied": { + "message": "Bu ayara kurumsal ilke gereksinimleri uygulandı" + }, + "fileSavedToDevice": { + "message": "Dosya cihaza kaydedildi. Cihazınızın indirilenler klasöründen yönetebilirsiniz." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Çöp kutusundaki kayıtlar" + }, + "noItemsInTrash": { + "message": "Çöp kutusunda hiç kayıt yok" + }, + "noItemsInTrashDesc": { + "message": "Sildiğiniz kayıtlar burada görünecek ve 30 gün sonra kalıcı olarak silinecektir" + }, + "trashWarning": { + "message": "30 günden uzun süre çöp kutusunda duran kayıtlar otomatik olarak silinecektir" + }, + "restore": { + "message": "Geri yükle" + }, + "deleteForever": { + "message": "Kalıcı olarak sil" + }, + "noEditPermissions": { + "message": "Bu kaydı düzenleme yetkisine sahip değilsiniz" } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 7f02c5ecf9a..eb7affa78c7 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "Для доступу до сховища увійдіть в обліковий запис, або створіть новий." }, + "inviteAccepted": { + "message": "Запрошення прийнято" + }, "createAccount": { "message": "Створити обліковий запис" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Завершіть створення облікового запису, встановивши пароль" }, - "login": { - "message": "Увійти" - }, "enterpriseSingleSignOn": { "message": "Єдиний корпоративний вхід (SSO)" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Підказка для головного пароля (необов'язково)" }, + "joinOrganization": { + "message": "Приєднатися до організації" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Завершіть приєднання до цієї організації, встановивши головний пароль." + }, "tab": { "message": "Вкладка" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Копіювати код безпеки" }, + "copyName": { + "message": "Копіювати ім'я" + }, + "copyCompany": { + "message": "Копіювати компанію" + }, + "copySSN": { + "message": "Копіювати номер соціального страхування" + }, + "copyPassportNumber": { + "message": "Копіювати номер паспорта" + }, + "copyLicenseNumber": { + "message": "Копіювати номер ліцензії" + }, "autoFill": { "message": "Автозаповнення" }, @@ -117,7 +138,7 @@ "message": "Автозаповнення картки" }, "autoFillIdentity": { - "message": "Автозаповнення особистих даних" + "message": "Автозаповнення посвідчень" }, "generatePasswordCopied": { "message": "Генерувати пароль (з копіюванням)" @@ -141,7 +162,7 @@ "message": "Додати картку" }, "addIdentityMenu": { - "message": "Додати особисті дані" + "message": "Додати посвідчення" }, "unlockVaultMenu": { "message": "Розблокуйте сховище" @@ -280,6 +301,24 @@ "editFolder": { "message": "Редагування" }, + "newFolder": { + "message": "Нова тека" + }, + "folderName": { + "message": "Назва теки" + }, + "folderHintText": { + "message": "Зробіть теку вкладеною, вказавши після основної теки \"/\". Наприклад: Обговорення/Форуми" + }, + "noFoldersAdded": { + "message": "Немає доданих тек" + }, + "createFoldersToOrganize": { + "message": "Створіть теки для організації записів у сховищі" + }, + "deleteFolderPermanently": { + "message": "Ви дійсно хочете остаточно видалити цю теку?" + }, "deleteFolder": { "message": "Видалити теку" }, @@ -345,16 +384,56 @@ "message": "Мінімальна довжина пароля" }, "uppercase": { - "message": "Верхній регістр (A-Z)" + "message": "Верхній регістр (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Нижній регістр (a-z)" + "message": "Нижній регістр (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Числа (0-9)" + "message": "Числа (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Спеціальні символи (!@#$%^&*)" + "message": "Спеціальні символи (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Включити", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Символи верхнього регістру", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Символи нижнього регістру", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Цифри", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Спеціальні символи", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Кількість слів" @@ -376,7 +455,12 @@ "message": "Мінімум спеціальних символів" }, "avoidAmbChar": { - "message": "Уникати неоднозначних символів" + "message": "Уникати неоднозначних символів", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Уникати неоднозначних символів", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Пошук" @@ -556,6 +640,18 @@ "security": { "message": "Безпека" }, + "confirmMasterPassword": { + "message": "Підтвердьте головний пароль" + }, + "masterPassword": { + "message": "Головний пароль" + }, + "masterPassImportant": { + "message": "Головний пароль неможливо відновити, якщо ви його втратите!" + }, + "masterPassHintLabel": { + "message": "Підказка для головного пароля" + }, "errorOccurred": { "message": "Сталася помилка" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Ваш обліковий запис створений! Тепер ви можете увійти." }, + "newAccountCreated2": { + "message": "Ваш обліковий запис створено!" + }, + "youHaveBeenLoggedIn": { + "message": "Ви увійшли!" + }, "youSuccessfullyLoggedIn": { "message": "Ви успішно увійшли в систему" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Потрібний код підтвердження." }, + "webauthnCancelOrTimeout": { + "message": "Автентифікацію було скасовано або вона тривала надто довго. Повторіть спробу." + }, "invalidVerificationCode": { "message": "Недійсний код підтвердження" }, @@ -613,7 +718,7 @@ } }, "autofillError": { - "message": "Не вдається заповнити пароль на цій сторінці. Скопіюйте і вставте ім'я користувача та/або пароль." + "message": "Не вдається заповнити вибраний запис на цій сторінці. Скопіюйте і вставте інформацію вручну." }, "totpCaptureError": { "message": "Неможливо сканувати QR-код з поточної сторінки" @@ -624,6 +729,18 @@ "totpCapture": { "message": "Скануйте QR-код програмою автентифікації" }, + "totpHelperTitle": { + "message": "Спростіть двоетапну перевірку" + }, + "totpHelper": { + "message": "Bitwarden може автоматично заповнювати одноразові коди двоетапної перевірки. Скопіюйте і вставте ключ у це поле." + }, + "totpHelperWithCapture": { + "message": "Bitwarden може автоматично заповнювати одноразові коди двоетапної перевірки. Відкрийте камеру, щоб сканувати QR-код на цьому вебсайті, або скопіюйте і вставте ключ у це поле." + }, + "learnMoreAboutAuthenticators": { + "message": "Докладніше про програми автентифікації" + }, "copyTOTP": { "message": "Скопіюйте ключ автентифікації (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "Тривалість вашого сеансу завершилась." }, + "logIn": { + "message": "Увійти" + }, + "restartRegistration": { + "message": "Перезапустити реєстрацію" + }, + "expiredLink": { + "message": "Протерміноване посилання" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Перезапустіть реєстрацію або спробуйте ввійти." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Можливо, ви вже зареєстровані" + }, "logOutConfirmation": { "message": "Ви дійсно хочете вийти?" }, @@ -649,7 +781,7 @@ "message": "Сталася неочікувана помилка." }, "nameRequired": { - "message": "Потрібна назва." + "message": "Необхідно ввести назву." }, "addedFolder": { "message": "Теку додано" @@ -697,6 +829,10 @@ "newUri": { "message": "Новий URI" }, + "addDomain": { + "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": { "message": "Запис додано" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Запитувати про додавання запису" }, + "vaultSaveOptionsTitle": { + "message": "Параметри збереження до сховища" + }, "addLoginNotificationDesc": { "message": "Запитувати про додавання запису, якщо його немає у вашому сховищі." }, "addLoginNotificationDescAlt": { "message": "Запитувати про додавання запису, якщо такого не знайдено у вашому сховищі. Застосовується для всіх облікових записів, до яких виконано вхід." }, + "showCardsInVaultView": { + "message": "Показувати картки як пропозиції автозаповнення в режимі перегляду сховища" + }, "showCardsCurrentTab": { "message": "Показувати картки на вкладці" }, "showCardsCurrentTabDesc": { "message": "Показувати список карток на сторінці вкладки для легкого автозаповнення." }, + "showIdentitiesInVaultView": { + "message": "Показувати особисті дані як пропозиції автозаповнення в режимі перегляду сховища" + }, "showIdentitiesCurrentTab": { "message": "Показувати посвідчення на вкладці" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Типове виявлення збігів URI", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Оберіть типовий спосіб виявлення збігів URI для виконання автозаповнення під час входу." @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 ГБ зашифрованого сховища для файлів." }, + "premiumSignUpEmergency": { + "message": "Екстрений доступ." + }, "premiumSignUpTwoStepOptions": { "message": "Додаткові можливості двоетапної авторизації, як-от YubiKey та Duo." }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Ви можете передплатити преміум у сховищі на bitwarden.com. Хочете перейти на вебсайт зараз?" }, + "premiumPurchaseAlertV2": { + "message": "Ви можете придбати Преміум у налаштуваннях облікового запису вебпрограмі Bitwarden." + }, "premiumCurrentMember": { "message": "Ви користуєтеся передплатою преміум!" }, "premiumCurrentMemberThanks": { "message": "Дякуємо за підтримку Bitwarden." }, + "premiumFeatures": { + "message": "Передплатіть преміум та отримайте:" + }, "premiumPrice": { "message": "Всього лише $PRICE$ / за рік!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "Усе лише за $PRICE$ на рік!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Оновлення завершено" }, @@ -1120,7 +1283,7 @@ "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { - "message": "Авторизуйтесь за допомогою Duo Security для вашої організації з використанням мобільного додатку Duo Mobile, SMS, телефонного виклику, або ключа безпеки U2F.", + "message": "Авторизуйтесь за допомогою Duo Security для вашої організації з використанням програми Duo Mobile, SMS, телефонного виклику, або ключа безпеки U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "webAuthnTitle": { @@ -1139,10 +1302,10 @@ "message": "Середовище власного хостингу" }, "selfHostedEnvironmentFooter": { - "message": "Вкажіть основну URL-адресу локально розміщеного встановлення Bitwarden." + "message": "Вкажіть основну URL-адресу вашого встановлення Bitwarden на власному хостингу." }, "selfHostedBaseUrlHint": { - "message": "Вкажіть основну URL-адресу вашого локально розміщеного встановлення Bitwarden. Зразок: https://bitwarden.company.com" + "message": "Вкажіть основну URL-адресу вашого встановлення Bitwarden на власному хостингу. Зразок: https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { "message": "Для розширеної конфігурації ви можете вказати основну URL-адресу окремо для кожної служби." @@ -1179,9 +1342,18 @@ }, "showAutoFillMenuOnFormFields": { "message": "Меню автозаповнення на полях форм", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Пропозиції автозаповнення" + }, + "showInlineMenuLabel": { + "message": "Пропозиції автозаповнення на полях форм" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Показувати пропозиції, якщо вибрано піктограму" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Застосовується для всіх облікових записів, до яких виконано вхід." }, "turnOffBrowserBuiltInPasswordManagerSettings": { @@ -1202,17 +1374,36 @@ "message": "Якщо вибрано піктограму автозаповнення", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Автозаповнення на сторінці" + }, "enableAutoFillOnPageLoad": { "message": "Автозаповнення на сторінці" }, "enableAutoFillOnPageLoadDesc": { "message": "Якщо виявлено форму входу, автоматично заповнювати її під час завантаження вебсторінки." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Обережно!$CLOSETAG$ Скомпрометовані або ненадійні вебсайти можуть використати функцію автозаповнення під час завантаження сторінки для завдання шкоди.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Скомпрометовані або ненадійні вебсайти можуть використати функцію автозаповнення під час завантаження сторінки для завдання шкоди." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Докладніше про ризики" + }, "learnMoreAboutAutofill": { - "message": "Дізнатися більше про автозаповнення" + "message": "Докладніше про автозаповнення" }, "defaultAutoFillOnPageLoad": { "message": "Типове налаштування автозаповнення для записів входу" @@ -1238,9 +1429,15 @@ "commandOpenSidebar": { "message": "Відкрити сховище у бічній панелі" }, - "commandAutofillDesc": { + "commandAutofillLoginDesc": { "message": "Автозаповнення останнього використаного запису для цього вебсайту" }, + "commandAutofillCardDesc": { + "message": "Автозаповнення останньої використаної картки для цього вебсайту" + }, + "commandAutofillIdentityDesc": { + "message": "Автозаповнення останнього використаного посвідчення для цього вебсайту" + }, "commandGeneratePasswordDesc": { "message": "Генерувати і копіювати новий випадковий пароль в буфер обміну" }, @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Логічне значення" }, + "cfTypeCheckbox": { + "message": "Прапорець" + }, "cfTypeLinked": { "message": "Пов'язано", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1391,7 +1591,7 @@ "message": "Повне ім'я" }, "identityName": { - "message": "Назва" + "message": "Назва посвідчення" }, "company": { "message": "Компанія" @@ -1451,7 +1651,7 @@ "message": "Картка" }, "typeIdentity": { - "message": "Особисті дані" + "message": "Посвідчення" }, "newItemHeader": { "message": "Новий $TYPE$", @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "Переглянути $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Історія паролів" }, @@ -1533,6 +1742,10 @@ "message": "Основний домен", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Основний домен (рекомендовано)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Ім'я домену", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Виявлення збігів", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Типове виявлення збігів", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Перемкнути налаштування" @@ -1578,11 +1791,20 @@ "message": "Типи" }, "allItems": { - "message": "Всі елементи" + "message": "Усі записи" }, "noPasswordsInList": { "message": "Немає паролів." }, + "clearHistory": { + "message": "Очистити історію" + }, + "noPasswordsToShow": { + "message": "Немає паролів" + }, + "noRecentlyGeneratedPassword": { + "message": "Ви не генерували паролі останнім часом" + }, "remove": { "message": "Вилучити" }, @@ -1605,7 +1827,7 @@ "message": "Ви впевнені, що ніколи не хочете блокувати? Встановивши цю опцію, ключ шифрування вашого сховища зберігатиметься на вашому пристрої. Використовуючи цю опцію, вам слід бути певними в тому, що ваш пристрій має належний захист." }, "noOrganizationsList": { - "message": "Ви не входите до жодної організації. Організації дозволяють безпечно обмінюватися елементами з іншими користувачами." + "message": "Ви не входите до жодної організації. Організації дають змогу безпечно обмінюватися записами з іншими користувачами." }, "noCollectionsInList": { "message": "Немає збірок." @@ -1614,7 +1836,7 @@ "message": "Власник" }, "whoOwnsThisItem": { - "message": "Хто є власником цього елемента?" + "message": "Хто є власником цього запису?" }, "strong": { "message": "Надійний", @@ -1677,6 +1899,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": "Дія після часу очікування сховища" }, @@ -1725,7 +1970,7 @@ "message": "Запис заповнено і збережено" }, "autoFillSuccess": { - "message": "Запис заповнено" + "message": "Запис заповнено " }, "insecurePageWarning": { "message": "Попередження: це незахищена сторінка HTTP, тому будь-яка інформація, яку ви передаєте, потенційно може бути переглянута чи змінена сторонніми. Ці облікові дані було збережено на безпечній сторінці (HTTPS)." @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Ваш новий головний пароль не задовольняє вимоги політики." }, - "receiveMarketingEmails": { - "message": "Отримуйте електронні листи від Bitwarden з оголошеннями, порадами та інформацією про нові можливості." + "receiveMarketingEmailsV2": { + "message": "Отримуйте поради, оголошення та можливості дослідження від Bitwarden у своїй поштовій скриньці." }, "unsubscribe": { "message": "Відписатися" @@ -1874,6 +2119,12 @@ "nativeMessagingWrongUserTitle": { "message": "Невідповідність облікових записів" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "Збій біометричного розблокування. Біометричний секретний ключ не зміг розблокувати сховище. Спробуйте налаштувати біометрію знову." + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "Біометричний ключ відрізняється" + }, "biometricsNotEnabledTitle": { "message": "Біометрію не налаштовано" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Розблокуйте цього користувача в програмі для комп'ютера і повторіть спробу." }, + "biometricsNotAvailableTitle": { + "message": "Біометричне розблокування недоступне" + }, + "biometricsNotAvailableDesc": { + "message": "Біометричне розблокування наразі недоступне. Повторіть спробу пізніше." + }, "biometricsFailedTitle": { "message": "Збій біометрії" }, @@ -1917,7 +2174,11 @@ "message": "Політика організації впливає на ваші параметри власності." }, "personalOwnershipPolicyInEffectImports": { - "message": "Політика організації заблокувала імпортування елементів до вашого особистого сховища." + "message": "Політика організації заблокувала імпортування записів до вашого особистого сховища." + }, + "domainsTitle": { + "message": "Домени", + "description": "A category title describing the concept of web domains" }, "excludedDomains": { "message": "Виключені домени" @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden не запитуватиме про збереження даних входу для цих доменів для всіх облікових записів, до яких виконано вхід. Потрібно оновити сторінку для застосування змін." }, + "websiteItemLabel": { + "message": "Вебсайт $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ не є дійсним доменом", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Виняток для домену збережено" + }, "send": { "message": "Відправлення", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "Захищено паролем" }, + "copyLink": { + "message": "Копіювати посилання" + }, "copySendLink": { "message": "Копіювати посилання відправлення", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Відправлення створено", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Відправлення успішно створено!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "Це відправлення буде доступним будь-кому за посиланням протягом $DAYS$ днів.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Посилання на відправлення скопійовано", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Відправлення збережено", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Необхідно підтвердити е-пошту" }, + "emailVerifiedV2": { + "message": "Електронну пошту підтверджено" + }, "emailVerificationRequiredDesc": { "message": "Для використання цієї функції необхідно підтвердити електронну пошту. Ви можете виконати підтвердження у вебсховищі." }, @@ -2179,6 +2476,9 @@ "updateWeakMasterPasswordWarning": { "message": "Ваш головний пароль не відповідає одній або більше політикам вашої організації. Щоб отримати доступ до сховища, вам необхідно оновити свій головний пароль зараз. Продовживши, ви вийдете з поточного сеансу, після чого потрібно буде повторно виконати вхід. Сеанси на інших пристроях можуть залишатися активними протягом однієї години." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Ваша організація вимкнула шифрування довірених пристроїв. Встановіть головний пароль для доступу до сховища." + }, "resetPasswordPolicyAutoEnroll": { "message": "Автоматичне розгортання" }, @@ -2310,7 +2610,7 @@ "message": "Експортування сховища організації" }, "exportingOrganizationVaultDesc": { - "message": "Буде експортовано лише сховище організації, пов'язане з $ORGANIZATION$. Елементи особистих сховищ або інших організацій не будуть включені.", + "message": "Буде експортовано лише сховище організації, пов'язане з $ORGANIZATION$. Записи особистих сховищ або інших організацій не будуть включені.", "placeholders": { "organization": { "content": "$1", @@ -2612,7 +2912,7 @@ "message": "Як працює автозаповнення" }, "autofillSelectInfoWithCommand": { - "message": "Виберіть елемент із цього екрану, скористайтеся комбінацією клавіш $COMMAND$, або дізнайтеся про інші можливості в налаштуваннях.", + "message": "Виберіть об'єкт із цього екрану, скористайтеся комбінацією клавіш $COMMAND$, або дізнайтеся про інші можливості в налаштуваннях.", "placeholders": { "command": { "content": "$1", @@ -2621,7 +2921,7 @@ } }, "autofillSelectInfoWithoutCommand": { - "message": "Виберіть елемент із цього екрану або дізнайтеся про інші можливості в налаштуваннях." + "message": "Виберіть об'єкт із цього екрану або дізнайтеся про інші можливості в налаштуваннях." }, "gotIt": { "message": "Зрозуміло" @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "Налаштування автозаповнення" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Комбінація клавіш автозаповнення" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Змінити комбінацію клавіш" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Керувати комбінаціями клавіш" + }, "autofillShortcut": { "message": "Комбінації клавіш автозаповнення" }, - "autofillShortcutNotSet": { + "autofillLoginShortcutNotSet": { "message": "Комбінацію клавіш для автозаповнення не встановлено. Змініть це в налаштуваннях браузера." }, - "autofillShortcutText": { - "message": "Комбінація клавіш автозаповнення: $COMMAND$. Змініть це в налаштуваннях браузера.", + "autofillLoginShortcutText": { + "message": "Комбінація клавіш автозаповнення: $COMMAND$. Керуйте всіма комбінаціями клавіш у налаштуваннях браузера.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "Довірений пристрій" }, + "sendsNoItemsTitle": { + "message": "Немає активних відправлень", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Використовуйте відправлення, щоб безпечно надавати доступ іншим до зашифрованої інформації.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "Необхідно ввести дані." }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 поле потребує вашої уваги." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ полів потребують вашої уваги.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- Оберіть--" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "Записи з повторним запитом головного пароля не можна автоматично заповнювати під час завантаження сторінки. Автозаповнення на сторінці вимкнено.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "Автозаповнення на сторінці налаштовано з типовими параметрами.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "Вимкніть повторний запит головного пароля, щоб редагувати це поле", @@ -2911,10 +3240,18 @@ "message": "Розблокуйте обліковий запис, щоб побачити відповідні записи", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Розблокуйте обліковий запис, щоб переглянути пропозиції автозаповнення", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "Розблокувати обліковий запис", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Розблокування облікового запису – відкриється нове вікно", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "Заповнити облікові дані для", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "Додати новий запис сховища", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "Новий запис", + "description": "Button text to display within inline menu when there are no matching items on a login field" + }, + "addNewLoginItemAria": { + "message": "Додавання нового запису для входу – відкриється нове вікно", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "Нова картка", + "description": "Button text to display within inline menu when there are no matching items on a credit card field" + }, + "addNewCardItemAria": { + "message": "Додавання нової картки – відкриється нове вікно", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "Нове посвідчення", + "description": "Button text to display within inline menu when there are no matching items on an identity field" + }, + "addNewIdentityItemAria": { + "message": "Додавання нового запису для посвідчення – відкриється нове вікно", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Доступне меню автозаповнення Bitwarden. Натисніть клавішу стрілки для вибору.", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -2965,7 +3326,7 @@ "message": "Дані успішно імпортовано" }, "importSuccessNumberOfItems": { - "message": "Всього імпортовано $AMOUNT$ елементів.", + "message": "Всього імпортовано $AMOUNT$ записів.", "placeholders": { "amount": { "content": "$1", @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Помилка під'єднання до служби Duo. Скористайтеся іншим способом двоетапної перевірки або зверніться до служби підтримки Duo по допомогу." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Запустіть Duo і виконайте дії для завершення входу." }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "Неправильний пароль файлу. Використайте пароль, який ви вводили під час створення експортованого файлу." }, - "importDestination": { - "message": "Призначення імпорту" + "destination": { + "message": "Призначення" }, "learnAboutImportOptions": { "message": "Дізнайтеся про параметри імпорту" @@ -3071,7 +3435,7 @@ } }, "importUnassignedItemsError": { - "message": "Файл містить непризначені елементи." + "message": "Файл містить непризначені записи." }, "selectFormat": { "message": "Оберіть формат імпортованого файлу" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "Сайт ініціює обов'язкову верифікацію. Ця функція ще не реалізована для облікових записів без головного пароля." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Увійти з ключем доступу?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "У вас немає відповідних записів для цього сайту." }, + "noMatchingLoginsForSite": { + "message": "Немає відповідних записів для цього сайту" + }, "confirm": { "message": "Підтвердити" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "Зберегти ключ доступу як новий запис" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Виберіть запис для збереження цього ключа доступу" }, + "chooseCipherForPasskeyAuth": { + "message": "Виберіть ключ доступу для входу" + }, "passkeyItem": { "message": "Ключ доступу" }, @@ -3153,7 +3523,7 @@ "message": "Перезаписати ключ доступу?" }, "overwritePasskeyAlert": { - "message": "Цей елемент вже містить ключ доступу. Ви впевнені, що хочете перезаписати поточний ключ доступу?" + "message": "Цей запис уже містить ключ доступу. Ви впевнені, що хочете перезаписати поточний ключ доступу?" }, "featureNotSupported": { "message": "Функція ще не підтримується" @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Поширені формати", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Відкрити налаштування браузера?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Відкрити довідковий центр?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" + }, + "confirmContinueToHelpCenterPasswordManagementContent": { + "message": "Змінити параметри автозаповнення та керування паролями браузера.", + "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": "Ви можете переглядати й керувати комбінаціями клавіш для розширень у налаштуваннях браузера.", + "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": "Змінити параметри автозаповнення та керування паролями браузера.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" + }, + "confirmContinueToBrowserKeyboardShortcutSettingsContent": { + "message": "Ви можете переглядати й керувати комбінаціями клавіш для розширень у налаштуваннях браузера.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Зробити Bitwarden типовим менеджером паролів?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Якщо ігнорувати цей параметр, можуть виникнути конфлікти автозаповнення між Bitwarden і браузером.", + "message": "Якщо ігнорувати цей параметр, можуть виникнути конфлікти пропозицій автозаповнення між Bitwarden і браузером.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Облікові дані успішно збережено!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Пароль збережено!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Облікові дані успішно оновлено!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Пароль оновлено!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Помилка збереження облікових даних. Перегляньте подробиці в консолі.", "description": "Notification message for when saving credentials has failed." @@ -3334,20 +3736,6 @@ "passkeyRemoved": { "message": "Ключ доступу вилучено" }, - "unassignedItemsBannerNotice": { - "message": "Примітка: непризначені елементи організації більше не видимі у поданні \"Усі сховища\" і доступні лише в консолі адміністратора." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Примітка: 16 травня 2024 року непризначені елементи організації більше не будуть видимі у поданні \"Усі сховища\" і будуть доступні лише через консоль адміністратора." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Призначте ці елементи збірці в", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "щоб зробити їх видимими.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "Пропозиції автозаповнення" }, @@ -3415,7 +3803,7 @@ }, "autofillTitle": { "message": "Автозаповнення – $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "Немає значень для копіювання" }, - "assignCollections": { - "message": "Призначити збірки" + "assignToCollections": { + "message": "Призначити до збірок" }, "copyEmail": { "message": "Копіювати е-пошту" @@ -3490,16 +3878,16 @@ } }, "itemsWithNoFolder": { - "message": "Елементи без теки" + "message": "Записи без теки" }, "itemDetails": { - "message": "Item details" + "message": "Подробиці запису" }, "itemName": { - "message": "Item name" + "message": "Назва запису" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Ви не можете вилучати збірки, маючи дозвіл лише на перегляд: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,14 +3899,32 @@ "message": "Організацію деактивовано" }, "owner": { - "message": "Owner" + "message": "Власник" }, "selfOwnershipLabel": { - "message": "You", + "message": "Ви", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { - "message": "Елементи в деактивованих організаціях недоступні. Зверніться до власника вашої організації для отримання допомоги." + "message": "Записи в деактивованих організаціях недоступні. Зверніться до власника вашої організації для отримання допомоги." + }, + "additionalInformation": { + "message": "Додаткова інформація" + }, + "itemHistory": { + "message": "Історія запису" + }, + "lastEdited": { + "message": "Востаннє редаговано" + }, + "ownerYou": { + "message": "Власник: Ви" + }, + "linked": { + "message": "Пов'язано" + }, + "copySuccessful": { + "message": "Успішно скопійовано" }, "upload": { "message": "Вивантажити" @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Фільтри" + }, + "personalDetails": { + "message": "Особисті дані" + }, + "identification": { + "message": "Ідентифікація" + }, + "contactInfo": { + "message": "Контактні дані" + }, + "downloadAttachment": { + "message": "Завантажити – $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "номер картки закінчується на", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Облікові дані для входу" + }, + "authenticatorKey": { + "message": "Ключ автентифікації" + }, + "autofillOptions": { + "message": "Параметри автозаповнення" + }, + "websiteUri": { + "message": "Вебсайт (URI)" + }, + "websiteUriCount": { + "message": "Вебсайт (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": "Вебсайт додано" + }, + "addWebsite": { + "message": "Додати вебсайт" + }, + "deleteWebsite": { + "message": "Видалити вебсайт" + }, + "defaultLabel": { + "message": "Типово ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Показати виявлення збігів $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Приховати виявлення збігів $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Автоматично заповнювати під час завантаження сторінки?" + }, + "cardExpiredTitle": { + "message": "Протермінована картка" + }, + "cardExpiredMessage": { + "message": "Якщо ви її поновили, оновіть інформацію" + }, + "cardDetails": { + "message": "Подробиці картки" + }, + "cardBrandDetails": { + "message": "Подробиці $BRAND$", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Увімкнути анімацію" + }, + "addAccount": { + "message": "Додати обліковий запис" + }, + "loading": { + "message": "Завантаження" + }, + "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": "Призначити" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Лише учасники організації з доступом до цих збірок зможуть переглядати запис." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Лише учасники організації з доступом до цих збірок зможуть переглядати записи." + }, + "bulkCollectionAssignmentWarning": { + "message": "Ви вибрали $TOTAL_COUNT$ записів. Ви не можете оновити $READONLY_COUNT$ записів, тому що у вас немає дозволу на редагування.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Додати поле" + }, + "add": { + "message": "Додати" + }, + "fieldType": { + "message": "Тип поля" + }, + "fieldLabel": { + "message": "Мітка поля" + }, + "textHelpText": { + "message": "Використовуйте текстові поля для даних, як-от секретні запитання" + }, + "hiddenHelpText": { + "message": "Використовуйте приховані поля для конфіденційних даних, як-от пароль" + }, + "checkBoxHelpText": { + "message": "Використовуйте прапорці, якщо хочете автоматично заповнювати поля, як-от адресу е-пошти" + }, + "linkedHelpText": { + "message": "Використовуйте пов'язане поле у разі виникнення проблем з автозаповненням для певного вебсайту." + }, + "linkedLabelHelpText": { + "message": "Введіть html-ідентифікатор поля, назву, мітку або заповнювач." + }, + "editField": { + "message": "Редагувати поле" + }, + "editFieldLabel": { + "message": "Редагувати $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Видалити $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ додано", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Перевпорядкувати $LABEL$. Використовуйте клавіші зі стрілками для переміщення.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ переміщено вгору, позиція $INDEX$ з $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Оберіть збірки для призначення" + }, + "personalItemTransferWarningSingular": { + "message": "1 запис буде остаточно перенесено до вибраної організації. Ви більше не будете власником цього запису." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ записів будуть остаточно перенесені до вибраної організації. Ви більше не будете власником цих записів.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 запис буде остаточно перенесено до $ORG$. Ви більше не будете власником цього запису.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ записів будуть остаточно перенесені до $ORG$. Ви більше не будете власником цих записів.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Збірки успішно призначено" + }, + "nothingSelected": { + "message": "Ви нічого не вибрали." + }, + "movedItemsToOrg": { + "message": "Вибрані записи переміщено до $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Записи переміщено до $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Запис переміщено до $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ переміщено вниз, позиція $INDEX$ з $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Розташування запису" + }, + "fileSends": { + "message": "Відправлення файлів" + }, + "textSends": { + "message": "Відправлення тексту" + }, + "bitwardenNewLook": { + "message": "Bitwarden має новий вигляд!" + }, + "bitwardenNewLookDesc": { + "message": "Ще простіше автозаповнення та інтуїтивніший пошук у сховищі. Ознайомтеся!" + }, + "accountActions": { + "message": "Дії з обліковим записом" + }, + "showNumberOfAutofillSuggestions": { + "message": "Показувати кількість пропозицій автозаповнення на піктограмі розширення" + }, + "systemDefault": { + "message": "Типово (система)" + }, + "enterprisePolicyRequirementsApplied": { + "message": "До цього налаштування застосовано вимоги політики компанії" + }, + "fileSavedToDevice": { + "message": "Файл збережено на пристрої. Ви можете його знайти у теці завантажень." + }, + "showCharacterCount": { + "message": "Показати кількість символів" + }, + "hideCharacterCount": { + "message": "Приховати кількість символів" + }, + "itemsInTrash": { + "message": "Записи в смітнику" + }, + "noItemsInTrash": { + "message": "Немає записів у смітнику" + }, + "noItemsInTrashDesc": { + "message": "Видалені записи з'являтимуться тут і будуть остаточно видалені через 30 днів" + }, + "trashWarning": { + "message": "Записи, що знаходяться в смітнику понад 30 днів, автоматично видалятимуться" + }, + "restore": { + "message": "Відновити" + }, + "deleteForever": { + "message": "Видалити остаточно" + }, + "noEditPermissions": { + "message": "Вам не дозволено редагувати цей запис" } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 3c87f71736b..1b059e1b9cc 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -3,27 +3,27 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Trình Quản lý Mật khẩu", + "message": "Trình quản lý mật khẩu Bitwarden", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Ở nhà, ở cơ quan, hay trên đường đi, Bitwarden sẽ bảo mật tất cả mật khẩu, passkey, và thông tin cá nhân của bạn", + "message": "Ở nhà, ở cơ quan, hay trên đường đi, Bitwarden sẽ bảo mật tất cả mật khẩu, mã khoá, và thông tin cá nhân của bạn", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Đăng nhập hoặc tạo tài khoản mới để truy cập kho lưu trữ của bạn." }, + "inviteAccepted": { + "message": "Lời mời được chấp nhận" + }, "createAccount": { "message": "Tạo tài khoản" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Đặt mật khẩu mạnh" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" - }, - "login": { - "message": "Đăng nhập" + "message": "Hoàn thành việc tạo tài khoản của bạn bằng cách đặt mật khẩu" }, "enterpriseSingleSignOn": { "message": "Đăng nhập bằng tài khoản tổ chức" @@ -50,7 +50,7 @@ "message": "Gợi ý mật khẩu chính có thể giúp bạn nhớ lại mật khẩu của mình nếu bạn quên nó." }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Nếu bạn quên mật khẩu, gợi ý mật khẩu có thể được gửi tới email của bạn. $CURRENT$/$MAXIMUM$ ký tự tối đa.", "placeholders": { "current": { "content": "$1", @@ -68,6 +68,12 @@ "masterPassHint": { "message": "Gợi ý mật khẩu chính (tùy chọn)" }, + "joinOrganization": { + "message": "Tham gia tổ chức" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Hoàn tất gia nhập tổ chức này bằng cách đặt một mật khẩu chính." + }, "tab": { "message": "Tab" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "Sao chép mã bảo mật" }, + "copyName": { + "message": "Sao chép tên" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Số bảo hiểm xã hội" + }, + "copyPassportNumber": { + "message": "Sao chép số hộ chiếu" + }, + "copyLicenseNumber": { + "message": "Sao chép số giấy phép" + }, "autoFill": { "message": "Tự động điền" }, @@ -123,7 +144,7 @@ "message": "Tạo mật khẩu (đã sao chép)" }, "copyElementIdentifier": { - "message": "Sao chép Tên trường Tùy chỉnh" + "message": "Sao chép tên trường tùy chỉnh" }, "noMatchingLogins": { "message": "Không có thông tin đăng nhập phù hợp." @@ -192,29 +213,29 @@ "message": "Tiếp tục tới ứng dụng web?" }, "continueToWebAppDesc": { - "message": "Explore more features of your Bitwarden account on the web app." + "message": "Khám phá thêm các tính năng của tài khoản Bitwarden của bạn trên bản web." }, "continueToHelpCenter": { - "message": "Continue to Help Center?" + "message": "Tiếp tục tới Trung tâm trợ giúp?" }, "continueToHelpCenterDesc": { - "message": "Learn more about how to use Bitwarden on the Help Center." + "message": "Tìm hiểu thêm về cách sử dụng Bitwarden trong Trung tâm trợ giúp." }, "continueToBrowserExtensionStore": { - "message": "Continue to browser extension store?" + "message": "Tiếp tục tới cửa hàng tiện ích mở rộng của trình duyệt?" }, "continueToBrowserExtensionStoreDesc": { - "message": "Help others find out if Bitwarden is right for them. Visit your browser's extension store and leave a rating now." + "message": "Giúp người khác tìm hiểu xem Bitwarden có phù hợp với họ không. Hãy truy cập cửa hàng tiện ích mở rộng trên trình duyệt của bạn và đánh giá ngay bây giờ." }, "changeMasterPasswordOnWebConfirmation": { "message": "Bạn có thể thay đổi mật khẩu chính của mình trên Bitwarden bản web." }, "fingerprintPhrase": { - "message": "Fingerprint Phrase", + "message": "Cụm vân tay", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." }, "yourAccountsFingerprint": { - "message": "Cụm từ mật khẩu của tài khoản của bạn", + "message": "Cụm vân tay của tài khoản của bạn", "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." }, "twoStepLogin": { @@ -224,43 +245,43 @@ "message": "Đăng xuất" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "Giới thiệu về Bitwarden" }, "about": { "message": "Thông tin" }, "moreFromBitwarden": { - "message": "More from Bitwarden" + "message": "Thông tin khác từ Bitwarden" }, "continueToBitwardenDotCom": { - "message": "Continue to bitwarden.com?" + "message": "Tiếp tục tới bitwarden.com?" }, "bitwardenForBusiness": { - "message": "Bitwarden for Business" + "message": "Bitwarden dành cho Doanh Nghiệp" }, "bitwardenAuthenticator": { "message": "Bitwarden Authenticator" }, "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": "Ứng dụng Bitwarden Authenticator cho phép bạn lưu trữ khóa xác thực và tạo mã TOTP cho quy trình xác minh hai bước. Tìm hiểu thêm trên trang web bitwarden.com" }, "bitwardenSecretsManager": { "message": "Bitwarden Secrets Manager" }, "continueToSecretsManagerPageDesc": { - "message": "Securely store, manage, and share developer secrets with Bitwarden Secrets Manager. Learn more on the bitwarden.com website." + "message": "Lưu trữ bảo mật, quản lý và chia sẻ bí mật của nhà phát triển với Bitwarden Secrets Manager. Truy cập bitwarden.com để biết thêm chi tiết." }, "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": "Tạo trải nghiệm đăng nhập mượt mà và an toàn không cần mật khẩu truyền thống với Passwordless.dev. Tìm hiểu thêm trên trang web bitwarden.com." }, "freeBitwardenFamilies": { - "message": "Free Bitwarden Families" + "message": "Gói Gia đình Miễn phí của Bitwarden" }, "freeBitwardenFamiliesPageDesc": { - "message": "You are eligible for Free Bitwarden Families. Redeem this offer today in the web app." + "message": "Bạn đủ điều kiện cho Gói Gia đình Miễn phí của Bitwarden. Hãy nhận ưu đãi này ngay hôm nay trên ứng dụng web." }, "version": { "message": "Phiên bản" @@ -280,6 +301,24 @@ "editFolder": { "message": "Chỉnh sửa thư mục" }, + "newFolder": { + "message": "Thư mục mới" + }, + "folderName": { + "message": "Tên thư mục" + }, + "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" }, @@ -311,17 +350,17 @@ "message": "Đồng bộ lần cuối:" }, "passGen": { - "message": "Tạo mật khẩu" + "message": "Trình tạo mật khẩu" }, "generator": { - "message": "Tạo mật khẩu", + "message": "Trình tạo", "description": "Short for 'Password Generator'." }, "passGenInfo": { "message": "Tự động tạo mật khẩu mạnh mẽ, độc nhất cho đăng nhập của bạn." }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "Ứng dụng Bitwarden bản web" }, "importItems": { "message": "Nhập mục" @@ -345,16 +384,56 @@ "message": "Độ dài mật khẩu tối thiểu" }, "uppercase": { - "message": "Chữ in hoa (A-Z)" + "message": "Chữ in hoa (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "Chữ in thường (a-z)" + "message": "Chữ in thường (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Chữ số (0-9)" + "message": "Chữ số (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "Ký tự đặc biệt (!@#$%^&*)" + "message": "Ký tự đặc biệt (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Bao gồm", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Bao gồm cả số", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Bao gồm các ký tự đặc biệt", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "Số từ" @@ -376,7 +455,12 @@ "message": "Số kí tự đặc biệt tối thiểu" }, "avoidAmbChar": { - "message": "Tránh các ký tự không rõ ràng" + "message": "Tránh các ký tự không rõ ràng", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "Tìm kiếm trong kho lưu trữ" @@ -409,13 +493,13 @@ "message": "Yêu thích" }, "unfavorite": { - "message": "Unfavorite" + "message": "Bỏ thích" }, "itemAddedToFavorites": { - "message": "Item added to favorites" + "message": "Đã thêm vào yêu thích" }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "Đã xóa khỏi yêu thích" }, "notes": { "message": "Ghi chú" @@ -439,7 +523,7 @@ "message": "Khởi chạy" }, "launchWebsite": { - "message": "Launch website" + "message": "Mở trang web" }, "website": { "message": "Trang web" @@ -454,7 +538,7 @@ "message": "Khác" }, "unlockMethods": { - "message": "Unlock options" + "message": "Tùy chọn mở khóa" }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Thiết lập phương thức mở khóa để thay đổi hành động hết thời gian chờ của vault." @@ -463,10 +547,10 @@ "message": "Thiết lập phương pháp mở khóa trong Cài đặt" }, "sessionTimeoutHeader": { - "message": "Session timeout" + "message": "Thời gian chờ của phiên" }, "otherOptions": { - "message": "Other options" + "message": "Tùy chọn khác" }, "rateExtension": { "message": "Đánh giá tiện ích mở rộng" @@ -481,7 +565,7 @@ "message": "Xác minh danh tính" }, "yourVaultIsLocked": { - "message": "Kho lưu trữ của bạn đã bị khóa. Hãy xác minh danh tính của bạn để mở khoá." + "message": "Kho của bạn đã bị khóa. Xác minh danh tính của bạn để mở khoá." }, "unlock": { "message": "Mở khóa" @@ -503,7 +587,7 @@ "message": "Mật khẩu chính không hợp lệ" }, "vaultTimeout": { - "message": "Thời gian chờ của kho lưu trữ" + "message": "Thời gian chờ của kho" }, "lockNow": { "message": "Khóa ngay" @@ -556,6 +640,18 @@ "security": { "message": "Bảo mật" }, + "confirmMasterPassword": { + "message": "Nhập lại mật khẩu chính" + }, + "masterPassword": { + "message": "Mật khẩu chính" + }, + "masterPassImportant": { + "message": "Mật khẩu chính của bạn không thể phục hồi nếu bạn quên nó!" + }, + "masterPassHintLabel": { + "message": "Gợi ý mật khẩu chính" + }, "errorOccurred": { "message": "Đã xảy ra lỗi" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "Tài khoản mới của bạn đã được tạo! Bạn có thể đăng nhập từ bây giờ." }, + "newAccountCreated2": { + "message": "Tài khoản của bạn đã được tạo thành công!" + }, + "youHaveBeenLoggedIn": { + "message": "Bạn đã đăng nhập thành công!" + }, "youSuccessfullyLoggedIn": { "message": "Bạn đã đăng nhập thành công" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "Yêu cầu mã xác nhận." }, + "webauthnCancelOrTimeout": { + "message": "Quá trình xác thực đã bị hủy hoặc mất quá nhiều thời gian. Vui lòng thử lại." + }, "invalidVerificationCode": { "message": "Mã xác minh không đúng" }, @@ -624,6 +729,18 @@ "totpCapture": { "message": "Quét mã QR xác thực từ trang web hiện tại" }, + "totpHelperTitle": { + "message": "Thực hiện xác minh hai bước liền mạch" + }, + "totpHelper": { + "message": "Bitwarden có thể lưu trữ và điền mã xác minh 2 bước. Sao chép và dán khóa vào trường này." + }, + "totpHelperWithCapture": { + "message": "Bitwarden có thể lưu trữ và điền mã xác minh 2 bước. Hãy chọn biểu tượng máy ảnh để chụp mã QR xác thực của trang web, hoặc sao chép và dán khoá vào ô này." + }, + "learnMoreAboutAuthenticators": { + "message": "Learn more about authenticators" + }, "copyTOTP": { "message": "Sao chép khóa Authenticator (TOTP)" }, @@ -631,11 +748,26 @@ "message": "Đã đăng xuất" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "Bạn đã đăng xuất khỏi tài khoản của mình." }, "loginExpired": { "message": "Phiên đăng nhập của bạn đã hết hạn." }, + "logIn": { + "message": "Đăng nhập" + }, + "restartRegistration": { + "message": "Tiến hành đăng ký lại" + }, + "expiredLink": { + "message": "Liên kết đã hết hạn" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Vui lòng đăng ký lại hoặc thử đăng nhập." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Bạn có thể đã có tài khoản" + }, "logOutConfirmation": { "message": "Bạn có chắc chắn muốn đăng xuất không?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "URI mới" }, + "addDomain": { + "message": "Thêm tên miền", + "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": { "message": "Đã thêm mục" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "Hỏi để thêm đăng nhập" }, + "vaultSaveOptionsTitle": { + "message": "Lưu vào các tùy chọn kho" + }, "addLoginNotificationDesc": { "message": "'Thông báo Thêm đăng nhập' sẽ tự động nhắc bạn lưu các đăng nhập mới vào hầm an toàn của bạn bất cứ khi nào bạn đăng nhập trang web lần đầu tiên." }, "addLoginNotificationDescAlt": { "message": "Đưa ra lựa chọn để thêm một mục nếu không tìm thấy mục đó trong hòm của bạn. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, + "showCardsInVaultView": { + "message": "Hiển thị các thẻ như các gợi ý tự động điền trên giao diện kho" + }, "showCardsCurrentTab": { "message": "Hiển thị thẻ trên trang Tab" }, "showCardsCurrentTabDesc": { "message": "Liệt kê các mục thẻ trên trang Tab để dễ dàng tự động điền." }, + "showIdentitiesInVaultView": { + "message": "Hiển thị các danh tính như các gợi ý tự động điền trên giao diện kho" + }, "showIdentitiesCurrentTab": { "message": "Hiển thị danh tính trên trang Tab" }, @@ -779,10 +924,10 @@ "message": "Đưa ra lựa chọn để cập nhật mật khẩu khi phát hiện có sự thay đổi trên trang web. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "enableUsePasskeys": { - "message": "Đưa ra lựa chọn để lưu và sử dụng passkey" + "message": "Đưa ra lựa chọn để lưu và sử dụng mã khoá" }, "usePasskeysDesc": { - "message": "Đưa ra lựa chọn để lưu passkey mới hoặc đăng nhập bằng passkey đã lưu trong hòm. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." + "message": "Đưa ra lựa chọn để lưu mã khoá mới hoặc đăng nhập bằng mã khoá đã lưu trong kho. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "notificationChangeDesc": { "message": "Bạn có muốn cập nhật mật khẩu này trên Bitwarden không?" @@ -797,7 +942,7 @@ "message": "Mở khóa" }, "additionalOptions": { - "message": "Additional options" + "message": "Tùy chọn bổ sung" }, "enableContextMenuItem": { "message": "Hiển thị tuỳ chọn menu ngữ cảnh" @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "Phương thức kiểm tra URI mặc định", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "Chọn phương thức mặc định để kiểm tra so sánh URI cho các đăng nhập khi xử lí các hành động như là tự động điền." @@ -837,62 +982,62 @@ "description": "'Solarized' is a noun and the name of a color scheme. It should not be translated." }, "exportFrom": { - "message": "Export from" + "message": "Xuất từ" }, "exportVault": { - "message": "Xuất kho lưu trữ" + "message": "Xuất kho" }, "fileFormat": { - "message": "File Format" + "message": "Định dạng tập tin" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "Tập tin xuất này sẽ được bảo vệ bằng mật khẩu và yêu cầu mật khẩu để giải mã." }, "filePassword": { - "message": "File password" + "message": "Mật khẩu tập tin" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "Mật khẩu này sẽ được sử dụng để xuất và nhập tập tin này" }, "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": "Sử dụng khóa mã hóa tài khoản của bạn, được tạo từ tên người dùng và mật khẩu chính của bạn để mã hóa tệp xuất và giới hạn việc nhập chỉ cho tài khoản Bitwarden hiện tại." }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "Thiết lập mật khẩu cho tệp để mã hóa dữ liệu xuất và nhập nó vào bất kỳ tài khoản Bitwarden nào bằng cách sử dụng mật khẩu đó để giải mã." }, "exportTypeHeading": { - "message": "Export type" + "message": "Loại xuất" }, "accountRestricted": { - "message": "Account restricted" + "message": "Tài khoản bị hạn chế" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "“Mật khẩu tập tin” và “Nhập lại mật khẩu tập tin” không khớp." }, "warning": { "message": "CẢNH BÁO", "description": "WARNING (should stay in capitalized letters if the language permits)" }, "confirmVaultExport": { - "message": "Xác nhận xuất kho lưu trữ" + "message": "Xác nhận xuất kho" }, "exportWarningDesc": { - "message": "This export contains your vault data in an unencrypted format. You should not store or send the exported file over unsecure channels (such as email). Delete it immediately after you are done using it." + "message": "Bản xuất này chứa dữ liệu kho bạn và không được mã hóa. Bạn không nên lưu trữ hay gửi tập tin đã xuất thông qua phương thức rủi ro (như email). Vui lòng xóa nó ngay lập tức khi bạn đã sử dụng xong." }, "encExportKeyWarningDesc": { - "message": "Quá trình xuất này sẽ mã hóa dữ liệu của bạn bằng khóa mã hóa của tài khoản. Nếu bạn từng xoay khóa mã hóa tài khoản của mình, bạn nên xuất lại vì bạn sẽ không thể giải mã tệp xuất này." + "message": "Quá trình xuất này sẽ mã hóa dữ liệu của bạn bằng khóa mã hóa của tài khoản. Nếu bạn từng xoay khóa mã hóa tài khoản của mình, bạn nên xuất lại vì bạn sẽ không thể giải mã tập tin xuất này." }, "encExportAccountWarningDesc": { - "message": "Các khóa mã hóa tài khoản là duy nhất cho mỗi tài khoản người dùng Bitwarden, vì vậy bạn không thể nhập một bản xuất được mã hóa vào một tài khoản khác." + "message": "Khóa mã hóa tài khoản là duy nhất cho mỗi tài khoản Bitwarden, vì vậy bạn không thể nhập tệp xuất được mã hóa vào một tài khoản khác." }, "exportMasterPassword": { - "message": "Nhập mật khẩu chính để xuất kho lưu trữ của bạn." + "message": "Nhập mật khẩu chính để xuất kho của bạn." }, "shared": { "message": "Đã chia sẻ" }, "bitwardenForBusinessPageDesc": { - "message": "Bitwarden for Business allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website." + "message": "Bitwarden cho Doanh Nghiệp cho phép bạn chia sẻ các mục trong kho mật khẩu với người khác bằng cách tạo một tổ chức. Tìm hiểu thêm trên bitwarden.com." }, "moveToOrganization": { "message": "Di chuyển đến tổ chức" @@ -956,13 +1101,13 @@ "message": "Chọn tập tin" }, "maxFileSize": { - "message": "Kích thước tối đa của tệp tin là 500MB." + "message": "Kích thước tối đa của tập tin là 500MB." }, "featureUnavailable": { "message": "Tính năng không có sẵn" }, "encryptionKeyMigrationRequired": { - "message": "Encryption key migration required. Please login through the web vault to update your encryption key." + "message": "Cần di chuyển khóa mã hóa. Vui lòng đăng nhập trang web Bitwaden để cập nhật khóa mã hóa của bạn." }, "premiumMembership": { "message": "Thành viên Cao Cấp" @@ -983,10 +1128,13 @@ "message": "Đăng ký làm thành viên cao cấp và nhận được:" }, "ppremiumSignUpStorage": { - "message": "1GB bộ nhớ lưu trữ tập tin được mã hóa." + "message": "1GB bộ nhớ lưu trữ được mã hóa cho các tệp đính kèm." + }, + "premiumSignUpEmergency": { + "message": "Emergency access." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Các tùy chọn xác minh hai bước như YubiKey và Duo." }, "ppremiumSignUpReports": { "message": "Thanh lọc mật khẩu, kiểm tra an toàn tài khoản và các báo cáo rò rĩ dữ liệu là để giữ cho kho của bạn an toàn." @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "Bạn có thể nâng cấp làm thành viên cao cấp trong kho bitwarden nền web. Bạn có muốn truy cập trang web bây giờ?" }, + "premiumPurchaseAlertV2": { + "message": "Bạn có thể mua gói Premium từ cài đặt tài khoản trên trang Bitwarden." + }, "premiumCurrentMember": { "message": "Bạn là một thành viên cao cấp!" }, "premiumCurrentMemberThanks": { "message": "Cảm ơn bạn vì đã hỗ trợ Bitwarden." }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "Tất cả chỉ với $PRICE$/năm!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "Tất cả chỉ với $PRICE$ /năm!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "Làm mới hoàn tất" }, @@ -1106,17 +1269,17 @@ "message": "Ứng dụng Authenticator" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Nhập mã được tạo bởi ứng dụng xác thực như Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP Security Key" + "message": "Khóa bảo mật OTP Yubico" }, "yubiKeyDesc": { "message": "Sử dụng YubiKey để truy cập tài khoản của bạn. Hoạt động với thiết bị YubiKey 4, 4 Nano, 4C và NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Nhập mã được tạo bởi Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1133,7 +1296,7 @@ "message": "Email" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Nhập mã được gửi về email của bạn." }, "selfHostedEnvironment": { "message": "Môi trường tự lưu trữ" @@ -1142,13 +1305,13 @@ "message": "Chỉ định liên kết cơ bản của cài đặt bitwarden tại chỗ của bạn." }, "selfHostedBaseUrlHint": { - "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" + "message": "Nhập địa chỉ cơ sở của bản cài đặt Bitwarden được lưu trữ tại máy chủ của bạn. Ví dụ: https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { - "message": "For advanced configuration, you can specify the base URL of each service independently." + "message": "Đối với cấu hình nâng cao. Bạn có thể chỉ định địa chỉ cơ sở của mỗi dịch vụ một cách độc lập." }, "selfHostedEnvFormInvalid": { - "message": "You must add either the base Server URL or at least one custom environment." + "message": "Bạn phải thêm địa chỉ máy chủ cơ sở hoặc ít nhất một môi trường tùy chỉnh." }, "customEnvironment": { "message": "Môi trường tùy chỉnh" @@ -1179,9 +1342,18 @@ }, "showAutoFillMenuOnFormFields": { "message": "Hiển thị menu tự động điền trên các trường biểu mẫu", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "Các gợi ý điền tự động" + }, + "showInlineMenuLabel": { + "message": "Hiển thị các gợi ý tự động điền trên các trường biểu mẫu" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Hiện gợi ý khi nhấp vào biểu tượng" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "Áp dụng cho tất cả tài khoản đã đăng nhập." }, "turnOffBrowserBuiltInPasswordManagerSettings": { @@ -1195,22 +1367,41 @@ "description": "Overlay setting select option for disabling autofill overlay" }, "autofillOverlayVisibilityOnFieldFocus": { - "message": "When field is selected (on focus)", + "message": "Khi trường được chọn (khi bấm vào)", "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { "message": "Khi chọn biểu tượng tự động điền", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Tự động điền khi tải trang" + }, "enableAutoFillOnPageLoad": { "message": "Tự động điền khi tải trang" }, "enableAutoFillOnPageLoadDesc": { "message": "Nếu phát hiện biểu mẫu đăng nhập, thực hiện tự động điền khi trang web tải xong." }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Cảnh báo:$CLOSETAG$ Các trang web bị xâm phạm hoặc không đáng tin cậy có thể lợi dụng tính năng tự động điền khi trang web được tải.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "Các trang web bị xâm phạm hoặc không đáng tin cậy có thể khai thác tính năng tự động điền khi tải trang." }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Tìm hiểu thêm về rủi ro" + }, "learnMoreAboutAutofill": { "message": "Tìm hiểu thêm về tự động điền" }, @@ -1238,14 +1429,20 @@ "commandOpenSidebar": { "message": "Mở kho ở thanh bên" }, - "commandAutofillDesc": { - "message": "Tự động điền thông tin đăng nhập người dùng cho trang web hiện tại." + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "Tạo và sao chép một mật khẩu ngẫu nhiên mới vào khay nhớ tạm" }, "commandLockVaultDesc": { - "message": "Khoá kho lưu trữ" + "message": "Khoá kho" }, "customFields": { "message": "Trường tùy chỉnh" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "Đúng/Sai" }, + "cfTypeCheckbox": { + "message": "Ô tích chọn" + }, "cfTypeLinked": { "message": "Đã liên kết", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1298,7 +1498,7 @@ "message": "Hiển thị biểu tượng bộ đếm" }, "badgeCounterDesc": { - "message": "Cho biết bạn có bao nhiêu lần đăng nhập cho trang web hiện tại." + "message": "Cho biết bạn có bao nhiêu thông tin đăng nhập cho trang web hiện tại." }, "cardholderName": { "message": "Tên chủ thẻ" @@ -1310,7 +1510,7 @@ "message": "Thương hiệu" }, "expirationMonth": { - "message": "Tháng Hết Hạn" + "message": "Tháng hết hạn" }, "expirationYear": { "message": "Năm hết hạn" @@ -1454,7 +1654,7 @@ "message": "Danh tính" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "$TYPE$ mới", "placeholders": { "type": { "content": "$1", @@ -1463,7 +1663,16 @@ } }, "editItemHeader": { - "message": "Edit $TYPE$", + "message": "Chỉnh sửa $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, + "viewItemHeader": { + "message": "Xem $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1481,7 +1690,7 @@ "message": "Bộ sưu tập" }, "nCollections": { - "message": "$COUNT$ collections", + "message": "$COUNT$ bộ sưu tập", "placeholders": { "count": { "content": "$1", @@ -1533,12 +1742,16 @@ "message": "Tên miền cơ sở", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "Tên miền", "description": "Domain name. Ex. website.com" }, "host": { - "message": "Máy chủ lưu trữ", + "message": "Máy chủ", "description": "A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'." }, "exact": { @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "Độ phù hợp", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "Độ phù hợp mặc định", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "Bật/tắt tùy chọn" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "Không có mật khẩu để liệt kê." }, + "clearHistory": { + "message": "Xóa lịch sử" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "Xoá" }, @@ -1598,11 +1820,11 @@ "description": "ex. Date this item was created" }, "datePasswordUpdated": { - "message": "Ngày cập nhật mật khẩu", + "message": "Đã cập nhật mật khẩu", "description": "ex. Date this password was updated" }, "neverLockWarning": { - "message": "Bạn có chắc bạn muốn sử dụng tùy chọn \"Không bao giờ\"? Đặt các tùy chọn khóa về \"Không bao giờ\" sẽ lưu key mã hóa kho của ngay trên thiết bị của bạn. Nếu bạn sử dụng tùy chọn này, bạn nên chắc chắn là thiết bị bạn đang được bảo vệ." + "message": "Bạn có chắc chắn muốn chọn \"Không bao giờ\" không? Lựa chọn này sẽ lưu khóa mã hóa kho của bạn trực tiếp trên thiết bị. Hãy nhớ bảo vệ thiết bị của bạn thật cẩn thận nếu bạn chọn tùy chọn này." }, "noOrganizationsList": { "message": "You do not belong to any organizations. Organizations allow you to securely share items with other users." @@ -1675,7 +1897,30 @@ "message": "Tạo bản sao" }, "passwordGeneratorPolicyInEffect": { - "message": "Có một hoặc vài chính sách của tổ chức đang làm ảnh hưởng đến cài đặt tạo mật khẩu của bạn." + "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": "Trình tạo mật khẩu" + }, + "usernameGenerator": { + "message": "Bộ tạo tên người dùng" + }, + "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ữ" @@ -1707,10 +1952,10 @@ "message": "Mục đã được khôi phục" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Bạn đã có tài khoản?" }, "vaultTimeoutLogOutConfirmation": { - "message": "Việc đăng xuất sẽ loại bỏ tất cả truy cập vào kho lưu trữ của bạn và yêu cầu xác minh trực tuyến sau khi hết giai đoạn thời gian chờ. Bạn có chắc chắn muốn dùng cài đặt này không?" + "message": "Đăng xuất sẽ xóa tất cả quyền truy cập vào kho của bạn và yêu cầu xác minh trực tuyến sau khi hết thời gian chờ. Bạn có chắc chắn muốn sử dụng cài đặt này không?" }, "vaultTimeoutLogOutConfirmationTitle": { "message": "Xác nhận hành động khi hết thời gian chờ" @@ -1719,7 +1964,7 @@ "message": "Tự động điền và Lưu" }, "fillAndSave": { - "message": "Fill and save" + "message": "Điền và lưu" }, "autoFillSuccessAndSavedUri": { "message": "Đã tự động điền mục và lưu URI" @@ -1799,20 +2044,20 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Mật khẩu chính bạn chọn không đáp ứng yêu cầu." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Nhận đề xuất, thông báo và cơ hội nghiên cứu từ Bitwarden trong hộp thư đến của bạn." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "Hủy đăng ký" }, "atAnyTime": { - "message": "at any time." + "message": "bất cứ lúc nào." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Nếu tiếp tục, bạn đồng ý" }, "and": { - "message": "and" + "message": "và" }, "acceptPolicies": { "message": "Bạn đồng ý với những điều sau khi nhấn chọn ô này:" @@ -1833,16 +2078,16 @@ "message": "Ok" }, "errorRefreshingAccessToken": { - "message": "Access Token Refresh Error" + "message": "Lỗi làm mới khoá truy cập" }, "errorRefreshingAccessTokenDesc": { - "message": "No refresh token or API keys found. Please try logging out and logging back in." + "message": "Bạn có thể đã bị đăng xuất. Vui lòng đăng xuất và đăng nhập lại." }, "desktopSyncVerificationTitle": { "message": "Xác minh đồng bộ máy tính" }, "desktopIntegrationVerificationText": { - "message": "Vui lòng xác minh rằng ứng dụng trên máy tính thấy vân tay này:" + "message": "Vui lòng xác minh ứng dụng trên máy tính hiển thị cụm vân tay này: " }, "desktopIntegrationDisabledTitle": { "message": "Tích hợp trình duyệt chưa được kích hoạt" @@ -1874,6 +2119,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" }, @@ -1887,16 +2138,22 @@ "message": "Nhận dạng sinh trắc học trên trình duyệt không được hỗ trợ trên thiết bị này" }, "biometricsNotUnlockedTitle": { - "message": "User locked or logged out" + "message": "Người dùng đã khoá hoặc đã đăng xuất" }, "biometricsNotUnlockedDesc": { - "message": "Please unlock this user in the desktop application and try again." + "message": "Vui lòng mở khóa người dùng này trong ứng dụng máy tính và thử lại." + }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." }, "biometricsFailedTitle": { - "message": "Biometrics failed" + "message": "Sinh trắc học không thành công" }, "biometricsFailedDesc": { - "message": "Biometrics cannot be completed, consider using a master password or logging out. If this persists, please contact Bitwarden support." + "message": "Không thể hoàn thành sinh trắc học, hãy cân nhắc sử dụng mật khẩu chính hoặc đăng xuất. Nếu sự cố vẫn tiếp diễn, vui lòng liên hệ bộ phận hỗ trợ của Bitwarden." }, "nativeMessaginPermissionErrorTitle": { "message": "Quyền chưa được cấp" @@ -1917,7 +2174,11 @@ "message": "Chính sách của tổ chức đang ảnh hưởng đến các tùy chọn quyền sở hữu của bạn." }, "personalOwnershipPolicyInEffectImports": { - "message": "An organization policy has blocked importing items into your individual vault." + "message": "Chính sách của tổ chức đã chặn việc nhập các mục vào kho cá nhân của bạn." + }, + "domainsTitle": { + "message": "Các tên miền", + "description": "A category title describing the concept of web domains" }, "excludedDomains": { "message": "Tên miền đã loại trừ" @@ -1926,27 +2187,39 @@ "message": "Bitwarden sẽ không yêu cầu lưu thông tin đăng nhập cho các miền này. Bạn phải làm mới trang để các thay đổi có hiệu lực." }, "excludedDomainsDescAlt": { - "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." + "message": "Bitwarden sẽ không yêu cầu lưu thông tin đăng nhập cho các miền này. Bạn phải làm mới trang để các thay đổi có hiệu lực." + }, + "websiteItemLabel": { + "message": "Trang Web $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ không phải là tên miền hợp lệ", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Các thay đổi tên miền loại trừ đã được lưu" + }, "send": { - "message": "Send", + "message": "Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "searchSends": { - "message": "Tìm kiếm Send", + "message": "Tìm kiếm mục Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "addSend": { - "message": "Thêm Send", + "message": "Thêm mục Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeText": { @@ -1956,11 +2229,11 @@ "message": "Tập tin" }, "allSends": { - "message": "Toàn bộ Send", + "message": "Tất cả mục Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "maxAccessCountReached": { - "message": "Đã đạt đến số lượng truy cập tối đa", + "message": "Đã vượt số lần truy cập tối đa", "description": "This text will be displayed after a Send has been accessed the maximum amount of times." }, "expired": { @@ -1972,8 +2245,11 @@ "passwordProtected": { "message": "Mật khẩu đã được bảo vệ" }, + "copyLink": { + "message": "Sao chép liên kết" + }, "copySendLink": { - "message": "Sao chép liên kết Send", + "message": "Sao chép liên kết mục Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "removePassword": { @@ -1986,11 +2262,11 @@ "message": "Đã xóa mật khẩu" }, "deletedSend": { - "message": "Đã xóa Send", + "message": "Đã xóa mục Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLink": { - "message": "Gửi liên kết", + "message": "Liên kết Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "disabled": { @@ -2000,15 +2276,15 @@ "message": "Bạn có chắc chắn muốn xóa mật khẩu này không?" }, "deleteSend": { - "message": "Xóa Send", + "message": "Xóa mục Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deleteSendConfirmation": { - "message": "Bạn có chắc chắn muốn xóa Send này?", + "message": "Bạn có chắc muốn mục Gửi này?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editSend": { - "message": "Chỉnh sửa Send", + "message": "Sửa mục Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeHeader": { @@ -2026,14 +2302,14 @@ "message": "Ngày xóa" }, "deletionDateDesc": { - "message": "Send sẽ được xóa vĩnh viễn vào ngày và giờ được chỉ định.", + "message": "Mục Gửi sẽ được xóa vĩnh viễn vào ngày và giờ chỉ định.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "expirationDate": { "message": "Ngày hết hạn" }, "expirationDateDesc": { - "message": "Nếu được thiết lập, truy cập vào Send này sẽ hết hạn vào ngày và giờ được chỉ định.", + "message": "Nếu được thiết lập, mục Gửi này sẽ hết hạn vào ngày và giờ được chỉ định.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "oneDay": { @@ -2055,11 +2331,11 @@ "message": "Số lượng truy cập tối đa" }, "maximumAccessCountDesc": { - "message": "Nếu được thiết lập, khi đã đạt tới số lượng truy cập tối đa, người dùng sẽ không thể truy cập Send này nữa.", + "message": "Nếu được thiết lập, khi đã đạt tới số lượng truy cập tối đa, người dùng sẽ không thể truy cập mục Gửi này nữa.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendPasswordDesc": { - "message": "Tùy chọn yêu cầu mật khẩu để người dùng truy cập Gửi này.", + "message": "Yêu cầu nhập mật khẩu khi người dùng truy cập vào phần Gửi này.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2067,7 +2343,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDisableDesc": { - "message": "Deactivate this Send so that no one can access it.", + "message": "Vô hiệu hoá mục Gửi này để không ai có thể truy cập nó.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendShareDesc": { @@ -2096,31 +2372,49 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDisabledWarning": { - "message": "Do chính sách doanh nghiệp, bạn chỉ có thể xóa những Send hiện có.", + "message": "Do chính sách doanh nghiệp, bạn chỉ có thể xóa những mục Gửi hiện có.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSend": { - "message": "Đã tạo Send", + "message": "Đã tạo mục Gửi", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editedSend": { - "message": "Đã chỉnh sửa Send", + "message": "Đã lưu mục Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLinuxChromiumFileWarning": { - "message": "In order to choose a file, open the extension in the sidebar (if possible) or pop out to a new window by clicking this banner." + "message": "Để chọn tập tin, mở tiện ích mở rộng trong thanh bên (nếu có thể) hoặc mở ra cửa sổ mới bằng cách nhấp vào biểu ngữ này." }, "sendFirefoxFileWarning": { - "message": "In order to choose a file using Firefox, open the extension in the sidebar or pop out to a new window by clicking this banner." + "message": "Để chọn tập tin bằng Firefox, mở tiện ích mở rộng trong thanh bên hoặc mở ra cửa sổ mới bằng cách nhấp vào biểu ngữ này." }, "sendSafariFileWarning": { - "message": "In order to choose a file using Safari, pop out to a new window by clicking this banner." + "message": "Để chọn tập tin bằng Safari, mở ra cửa sổ mới bằng cách nhấp vào biểu ngữ này." }, "sendFileCalloutHeader": { "message": "Trước khi bạn bắt đầu" }, "sendFirefoxCustomDatePopoutMessage1": { - "message": "To use a calendar style date picker", + "message": "Để dùng bộ chọn ngày dạng lịch", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" }, "sendFirefoxCustomDatePopoutMessage2": { @@ -2128,29 +2422,29 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" }, "sendFirefoxCustomDatePopoutMessage3": { - "message": "to pop out your window.", + "message": "để bật cửa sổ của bạn ra.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker click here **to pop out your window.**'" }, "expirationDateIsInvalid": { - "message": "The expiration date provided is not valid." + "message": "Ngày hết hạn bạn nhập không hợp lệ." }, "deletionDateIsInvalid": { - "message": "The deletion date provided is not valid." + "message": "Ngày xóa bạn nhập không hợp lệ." }, "expirationDateAndTimeRequired": { - "message": "An expiration date and time are required." + "message": "Ngày và giờ hết hạn là bắt buộc." }, "deletionDateAndTimeRequired": { - "message": "A deletion date and time are required." + "message": "Ngày và giờ xóa là bắt buộc." }, "dateParsingError": { - "message": "There was an error saving your deletion and expiration dates." + "message": "Đã xảy ra lỗi khi lưu ngày xoá và ngày hết hạn của bạn." }, "hideEmail": { - "message": "Hide my email address from recipients." + "message": "Ẩn địa chỉ email của tôi khỏi người nhận." }, "sendOptionsPolicyInEffect": { - "message": "One or more organization policies are affecting your Send options." + "message": "Các chính sách của tổ chức đang ảnh hưởng đến tùy chọn Gửi của bạn." }, "passwordPrompt": { "message": "Nhắc lại mật khẩu chính" @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "Yêu cầu xác nhận danh tính qua email" }, + "emailVerifiedV2": { + "message": "Email đã xác minh" + }, "emailVerificationRequiredDesc": { "message": "Bạn phải xác nhận email để sử dụng tính năng này. Bạn có thể xác minh email trên web." }, @@ -2174,34 +2471,37 @@ "message": "Cập nhật mật khẩu chính" }, "updateMasterPasswordWarning": { - "message": "Your master password was recently changed by an administrator in your organization. In order to access the vault, you must update it now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." + "message": "Mật khẩu chính của bạn gần đây đã được thay đổi bởi người quản trị trong tổ chức của bạn. Để truy cập kho, bạn phải cập nhật nó ngay bây giờ. Việc tiếp tục sẽ đăng xuất khỏi kho và bạn sẽ cần đăng nhập lại. Ứng dụng Bitwaden trên các thiết bị khác có thể tiếp tục hoạt động trong tối đa một giờ sau đó sẽ bị đăng xuất." }, "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." + "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": "Tổ chức của bạn đã vô hiệu hóa mã hóa bằng thiết bị đáng tin cậy. Vui lòng đặt mật khẩu chính để truy cập Kho của bạn." }, "resetPasswordPolicyAutoEnroll": { - "message": "Automatic enrollment" + "message": "Đăng ký tự động" }, "resetPasswordAutoEnrollInviteWarning": { - "message": "This organization has an enterprise policy that will automatically enroll you in password reset. Enrollment will allow organization administrators to change your master password." + "message": "Tổ chức này có chính sách doanh nghiệp sẽ tự động đặt lại mật khẩu chính cho bạn. Đăng ký sẽ cho phép quản trị viên tổ chức thay đổi mật khẩu chính của bạn." }, "selectFolder": { "message": "Chọn thư mục..." }, "noFoldersFound": { - "message": "No folders found", + "message": "Không tìm thấy thư mục nào", "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "Quyền tổ chức của bạn đã được cập nhật, yêu cầu bạn đặt mật khẩu chính.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "Tổ chức của bạn yêu cầu bạn đặt mật khẩu chính.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { - "message": "Verification required", + "message": "Yêu cầu xác minh", "description": "Default title for the user verification dialog." }, "hours": { @@ -2211,7 +2511,7 @@ "message": "Phút" }, "vaultTimeoutPolicyInEffect": { - "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", + "message": "Tổ chức của bạn đã đặt thời gian mở kho tối đa là $HOURS$ giờ và $MINUTES$ phút.", "placeholders": { "hours": { "content": "$1", @@ -2224,7 +2524,7 @@ } }, "vaultTimeoutPolicyWithActionInEffect": { - "message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", + "message": "Tổ chức của bạn đang ảnh hưởng đến thời gian mở kho. Thời gian mở kho tối đa là $HOURS$ giờ và $MINUTES$ phút. Kho sẽ $ACTION$ sau khi hết thời gian mở kho.", "placeholders": { "hours": { "content": "$1", @@ -2241,7 +2541,7 @@ } }, "vaultTimeoutActionPolicyInEffect": { - "message": "Your organization policies have set your vault timeout action to $ACTION$.", + "message": "Tổ chức của bạn sẽ $ACTION$ sau khi hết thời gian mở kho.", "placeholders": { "action": { "content": "$1", @@ -2250,22 +2550,22 @@ } }, "vaultTimeoutTooLarge": { - "message": "Your vault timeout exceeds the restrictions set by your organization." + "message": "Thời gian mở kho vượt quá giới hạn do tổ chức của bạn đặt ra." }, "vaultExportDisabled": { - "message": "Xuất kho lưu trữ không có sẵn" + "message": "Xuất kho không có sẵn" }, "personalVaultExportPolicyInEffect": { - "message": "One or more organization policies prevents you from exporting your individual vault." + "message": "Các chính sách của tổ chức ngăn cản bạn xuất kho lưu trữ cá nhân của mình." }, "copyCustomFieldNameInvalidElement": { - "message": "Unable to identify a valid form element. Try inspecting the HTML instead." + "message": "Không thể xác định được phần tử biểu mẫu hợp lệ. Thay vào đó hãy thử kiểm tra trong HTML." }, "copyCustomFieldNameNotUnique": { - "message": "No unique identifier found." + "message": "Không tìm thấy danh tính duy nhất." }, "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ hiện đang dùng SSO với khoá máy chủ tự lưu trữ. Từ giờ không cần mật khẩu chính để đăng nhập vào tổ chức này nữa.", + "message": "$ORGANIZATION$ đang sử dụng SSO với khóa máy chủ tự lưu trữ. Mật khẩu chính không còn cần để đăng nhập cho các thành viên của tổ chức này.", "placeholders": { "organization": { "content": "$1", @@ -2292,13 +2592,13 @@ "message": "Bật tắt đếm kí tự" }, "sessionTimeout": { - "message": "Your session has timed out. Please go back and try logging in again." + "message": "Phiên đăng nhập của bạn đã hết hạn. Vui lòng quay trở lại và thử đăng nhập lại." }, "exportingPersonalVaultTitle": { - "message": "Exporting individual vault" + "message": "Đang xuất dữ liệu kho cá nhân" }, "exportingIndividualVaultDescription": { - "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.", + "message": "Chỉ dữ liệu trong kho cá nhân liên kết với $EMAIL$ mới được xuất. Không bao gồm \ncác dữ liệu trong kho tổ chức. Chỉ thông tin mục kho mới được xuất, sẽ không có các tệp đính kèm.", "placeholders": { "email": { "content": "$1", @@ -2307,10 +2607,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "Đang xuất dữ liệu kho tổ chức" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Chỉ dữ liệu trong kho tổ chức $ORGANIZATION$ mới được xuất. Các kho cá nhân hoặc của tổ chức khác sẽ không được bao gồm.", "placeholders": { "organization": { "content": "$1", @@ -2335,13 +2635,13 @@ "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" }, "plusAddressedEmailDesc": { - "message": "Use your email provider's sub-addressing capabilities." + "message": "Sử dụng khả năng địa chỉ phụ của nhà cung cấp dịch vụ mail của bạn." }, "catchallEmail": { "message": "Email Catch-all" }, "catchallEmailDesc": { - "message": "Use your domain's configured catch-all inbox." + "message": "Sử dụng hộp thư bạn đã thiết lập để nhận tất cả email gửi đến tên miền của bạn." }, "random": { "message": "Ngẫu nhiên" @@ -2362,13 +2662,13 @@ "message": "Dịch vụ" }, "forwardedEmail": { - "message": "Forwarded email alias" + "message": "Đã chuyển tiếp bí danh email" }, "forwardedEmailDesc": { - "message": "Generate an email alias with an external forwarding service." + "message": "Tạo bí danh email với dịch vụ chuyển tiếp bên ngoài." }, "forwarderError": { - "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "message": "Lỗi $SERVICENAME$: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", "placeholders": { "servicename": { @@ -2382,11 +2682,11 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "Được tạo bởi Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "Trang web: $WEBSITE$. Được tạo bởi Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -2396,7 +2696,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "Khoá API $SERVICENAME$ không hợp lệ", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -2406,7 +2706,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "Khoá API $SERVICENAME$ không hợp lệ: $ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -2420,7 +2720,7 @@ } }, "forwarderNoAccountId": { - "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "message": "Không thể lấy ID tài khoản email ẩn từ $SERVICENAME$.", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -2430,7 +2730,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "Tên miền $SERVICENAME$ không hợp lệ.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -2440,7 +2740,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "Địa chỉ $SERVICENAME$ không hợp lệ.", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -2450,7 +2750,7 @@ } }, "forwarderUnknownError": { - "message": "Unknown $SERVICENAME$ error occurred.", + "message": "$SERVICENAME$ đã xảy ra lỗi không xác định.", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -2460,7 +2760,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Unknown forwarder: '$SERVICENAME$'.", + "message": "Người chuyển tiếp không xác định: '$SERVICENAME$'.", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -2480,16 +2780,16 @@ "message": "Khóa API" }, "ssoKeyConnectorError": { - "message": "Key connector error: make sure key connector is available and working correctly." + "message": "Lỗi kết nối khóa: hãy đảm bảo kết nối khóa khả dụng và hoạt động chính xác." }, "premiumSubcriptionRequired": { - "message": "Premium subscription required" + "message": "Yêu cầu đăng ký gói Premium" }, "organizationIsDisabled": { - "message": "Organization suspended." + "message": "Tổ chức đã ngưng hoạt động." }, "disabledOrganizationFilterError": { - "message": "Items in suspended Organizations cannot be accessed. Contact your Organization owner for assistance." + "message": "Không thể truy cập các mục trong tổ chức đã ngưng hoạt động. Hãy liên hệ với chủ sở hữu tổ chức để được hỗ trợ." }, "loggingInTo": { "message": "Đang đăng nhập vào $DOMAIN$", @@ -2513,13 +2813,13 @@ "message": "Phiên bản máy chủ" }, "selfHostedServer": { - "message": "self-hosted" + "message": "tự lưu trữ" }, "thirdParty": { "message": "Bên thứ ba" }, "thirdPartyServerMessage": { - "message": "Connected to third-party server implementation, $SERVERNAME$. Please verify bugs using the official server, or report them to the third-party server.", + "message": "Bạn đang kết nối đến máy chủ $SERVERNAME$ của bên thứ ba. Vui lòng kiểm tra lỗi bằng cách sử dụng máy chủ chính thức hoặc báo lỗi cho bên thứ ba.", "placeholders": { "servername": { "content": "$1", @@ -2543,7 +2843,7 @@ "message": "Đang đăng nhập với tên" }, "notYou": { - "message": "Không phải bạn sao?" + "message": "Không phải bạn?" }, "newAroundHere": { "message": "Bạn mới tới đây sao?" @@ -2555,13 +2855,13 @@ "message": "Đăng nhập bằng thiết bị" }, "loginWithDeviceEnabledInfo": { - "message": "Log in with device must be set up in the settings of the Bitwarden app. Need another option?" + "message": "Đăng nhập bằng thiết bị phải được thiết lập trong cài đặt của ứng dụng Bitwarden. Dùng cách khác?" }, "fingerprintPhraseHeader": { - "message": "Cụm từ dấu vân tay" + "message": "Cụm vân tay" }, "fingerprintMatchInfo": { - "message": "Please make sure your vault is unlocked and the Fingerprint phrase matches on the other device." + "message": "Vui lòng đảm bảo rằng bạn đã mở khoá kho và cụm vân tay khớp trên thiết bị khác." }, "resendNotification": { "message": "Gửi lại thông báo" @@ -2573,28 +2873,28 @@ "message": "Một thông báo đã được gửi đến thiết bị của bạn." }, "loginInitiated": { - "message": "Login initiated" + "message": "Bắt đầu đăng nhập" }, "exposedMasterPassword": { "message": "Mật khẩu chính bị lộ" }, "exposedMasterPasswordDesc": { - "message": "Password found in a data breach. Use a unique password to protect your account. Are you sure you want to use an exposed password?" + "message": "Mật khẩu này đã bị rò rỉ trong một vụ tấn công dữ liệu. Dùng mật khẩu mới và an toàn để bảo vệ tài khoản bạn. Bạn có chắc muốn sử dụng mật khẩu đã bị rò rỉ?" }, "weakAndExposedMasterPassword": { - "message": "Weak and Exposed Master Password" + "message": "Mật khẩu chính yếu và bị lộ" }, "weakAndBreachedMasterPasswordDesc": { - "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" + "message": "Mật khẩu yếu này đã bị rò rỉ trong một vụ tấn công dữ liệu. Dùng mật khẩu mới và an toàn để bảo vệ tài khoản bạn. Bạn có chắc muốn sử dụng mật khẩu đã bị rò rỉ?" }, "checkForBreaches": { - "message": "Check known data breaches for this password" + "message": "Kiểm tra mật khẩu có lộ trong các vụ rò rỉ dữ liệu hay không" }, "important": { "message": "Quan trọng:" }, "masterPasswordHint": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "Mật khẩu chính của bạn không thể phục hồi nếu bạn quên nó!" }, "characterMinimum": { "message": "$LENGTH$ ký tự tối thiểu", @@ -2612,7 +2912,7 @@ "message": "Cách tự đồng điền" }, "autofillSelectInfoWithCommand": { - "message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.", + "message": "Chọn một mục từ màn hình này, sử dụng phím tắt $COMMAND$, hoặc khám phá các tùy chọn khác trong cài đặt.", "placeholders": { "command": { "content": "$1", @@ -2621,22 +2921,31 @@ } }, "autofillSelectInfoWithoutCommand": { - "message": "Select an item from this screen, or explore other options in settings." + "message": "Chọn một mục từ màn hình này, hoặc khám phá các tùy chọn khác trong cài đặt." }, "gotIt": { - "message": "Got it" + "message": "Đã hiểu" }, "autofillSettings": { "message": "Cài đặt tự động điền" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Phím tắt tự động điền" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Thay đổi phím tắt" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Quản lý các lối tắt" + }, "autofillShortcut": { "message": "Phím tắt tự động điền" }, - "autofillShortcutNotSet": { - "message": "Chưa cài đặt phím tắt cho chức năng tự động điền. Vui lòng thay đổi trong cài đặt của trình duyệt." + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "Phím tắt cho chức năng tự động điền là $COMMAND$. Vui lòng thay đổi trong cài đặt của trình duyệt.", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2654,101 +2963,109 @@ } }, "loggingInOn": { - "message": "Logging in on" + "message": "Đang đăng nhập vào" }, "opensInANewWindow": { - "message": "Opens in a new window" + "message": "Mở trong cửa sổ mới" }, "deviceApprovalRequired": { - "message": "Device approval required. Select an approval option below:" + "message": "Yêu cầu phê duyệt thiết bị. Chọn một tuỳ chọn phê duyệt bên dưới:" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Lưu thiết bị này" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "Bỏ chọn nếu sử dụng thiết bị công cộng" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Phê duyệt bằng thiết bị khác" }, "requestAdminApproval": { - "message": "Request admin approval" + "message": "Yêu cầu quản trị viên phê duyệt" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "Phê duyệt bằng mật khẩu chính" }, "ssoIdentifierRequired": { - "message": "Organization SSO identifier is required." + "message": "Cần có mã định danh SSO của tổ chức." }, "creatingAccountOn": { - "message": "Creating account on" + "message": "Đang tạo tài khoản trên" }, "checkYourEmail": { - "message": "Check your email" + "message": "Kiểm tra email của bạn" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "Nhấp vào liên kết trong email được gửi đến" }, "andContinueCreatingYourAccount": { - "message": "and continue creating your account." + "message": "và tiếp tục tạo tài khoản của bạn." }, "noEmail": { - "message": "No email?" + "message": "Không có email?" }, "goBack": { - "message": "Go back" + "message": "Quay lại" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "để chỉnh sửa địa chỉ email của bạn." }, "eu": { - "message": "EU", + "message": "Châu Âu", "description": "European Union" }, "accessDenied": { - "message": "Access denied. You do not have permission to view this page." + "message": "Truy cập bị từ chối. Bạn không có quyền xem trang này." }, "general": { - "message": "General" + "message": "Chung" }, "display": { - "message": "Display" + "message": "Hiển thị" }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Tạo tài khoản thành công!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "Yêu cầu quản trị viên phê duyệt" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "Yêu cầu của bạn đã được gửi đến quản trị viên." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Bạn sẽ có thông báo nếu được phê duyệt." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Không thể đăng nhập?" }, "loginApproved": { - "message": "Login approved" + "message": "Lượt đăng nhập đã duyệt" }, "userEmailMissing": { - "message": "User email missing" + "message": "Thiếu email người dùng" }, "deviceTrusted": { - "message": "Device trusted" + "message": "Thiết bị tin cậy" + }, + "sendsNoItemsTitle": { + "message": "Không có mục Gửi nào đang hoạt động", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Sử dụng Gửi để chia sẻ thông tin mã hóa một cách an toàn với bất kỳ ai.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { - "message": "Input is required." + "message": "Trường này là bắt buộc." }, "required": { - "message": "required" + "message": "bắt buộc" }, "search": { - "message": "Search" + "message": "Tìm kiếm" }, "inputMinLength": { - "message": "Input must be at least $COUNT$ characters long.", + "message": "Giá trị nhập vào phải ít nhất $COUNT$ ký tự.", "placeholders": { "count": { "content": "$1", @@ -2757,7 +3074,7 @@ } }, "inputMaxLength": { - "message": "Input must not exceed $COUNT$ characters in length.", + "message": "Giá trị nhập vào không được vượt quá $COUNT$ ký tự.", "placeholders": { "count": { "content": "$1", @@ -2766,7 +3083,7 @@ } }, "inputForbiddenCharacters": { - "message": "The following characters are not allowed: $CHARACTERS$", + "message": "Các ký tự sau không được phép sử dụng: $CHARACTERS$", "placeholders": { "characters": { "content": "$1", @@ -2775,7 +3092,7 @@ } }, "inputMinValue": { - "message": "Input value must be at least $MIN$.", + "message": "Giá trị nhập vào phải ít nhất $MIN$.", "placeholders": { "min": { "content": "$1", @@ -2784,7 +3101,7 @@ } }, "inputMaxValue": { - "message": "Input value must not exceed $MAX$.", + "message": "Giá trị nhập vào không được vượt quá $MAX$.", "placeholders": { "max": { "content": "$1", @@ -2793,17 +3110,17 @@ } }, "multipleInputEmails": { - "message": "1 or more emails are invalid" + "message": "Có ít nhất 1 địa chỉ email không hợp lệ" }, "inputTrimValidator": { - "message": "Input must not contain only whitespace.", + "message": "Giá trị nhập vào không được chỉ có khoảng trắng.", "description": "Notification to inform the user that a form's input can't contain only whitespace." }, "inputEmail": { - "message": "Input is not an email address." + "message": "Giá trị nhập vào không phải là địa chỉ email." }, "fieldsNeedAttention": { - "message": "$COUNT$ field(s) above need your attention.", + "message": "Có $COUNT$ trường cần bạn xem xét ở trên.", "placeholders": { "count": { "content": "$1", @@ -2811,23 +3128,35 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { - "message": "-- Select --" + "message": "-- Chọn --" }, "multiSelectPlaceholder": { - "message": "-- Type to filter --" + "message": "-- Nhập để lọc --" }, "multiSelectLoading": { - "message": "Retrieving options..." + "message": "Đang tải các tuỳ chọn..." }, "multiSelectNotFound": { - "message": "No items found" + "message": "Không tìm thấy mục nào" }, "multiSelectClearAll": { - "message": "Clear all" + "message": "Xoá tất cả" }, "plusNMore": { - "message": "+ $QUANTITY$ more", + "message": "+ $QUANTITY$ nhiều hơn", "placeholders": { "quantity": { "content": "$1", @@ -2836,136 +3165,168 @@ } }, "submenu": { - "message": "Submenu" + "message": "Menu con" }, "toggleCollapse": { - "message": "Toggle collapse", + "message": "Bật/tắt thu gọn", "description": "Toggling an expand/collapse state." }, "filelessImport": { - "message": "Import your data to Bitwarden?", + "message": "Nhập dữ liệu của bạn vào Bitwarden?", "description": "Default notification title for triggering a fileless import." }, "lpFilelessImport": { - "message": "Protect your LastPass data and import to Bitwarden?", + "message": "Bảo vệ dữ liệu LastPass của bạn và nhập vào Bitwarden?", "description": "LastPass specific notification title for triggering a fileless import." }, "lpCancelFilelessImport": { - "message": "Save as unencrypted file", + "message": "Lưu dưới dạng tập tin không được mã hóa", "description": "LastPass specific notification button text for cancelling a fileless import." }, "startFilelessImport": { - "message": "Import to Bitwarden", + "message": "Nhập vào Bitwarden", "description": "Notification button text for starting a fileless import." }, "importing": { - "message": "Importing...", + "message": "Đang nhập...", "description": "Notification message for when an import is in progress." }, "dataSuccessfullyImported": { - "message": "Data successfully imported!", + "message": "Dữ liệu đã được nhập thành công!", "description": "Notification message for when an import has completed successfully." }, "dataImportFailed": { - "message": "Error importing. Check console for details.", + "message": "Xảy ra lỗi trong quá trình nhập. Kiểm tra bảng điều khiển để biết thêm chi tiết.", "description": "Notification message for when an import has failed." }, "importNetworkError": { - "message": "Network error encountered during import.", + "message": "Đã xảy ra lỗi mạng trong quá trình nhập dữ liệu.", "description": "Notification message for when an import has failed due to a network error." }, "aliasDomain": { - "message": "Alias domain" + "message": "Tên miền thay thế" }, "passwordRepromptDisabledAutofillOnPageLoad": { - "message": "Items with master password re-prompt cannot be auto-filled on page load. Auto-fill on page load turned off.", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "message": "Các mục yêu cầu nhập lại mật khẩu chính không thể tự động điền khi tải trang. Tự động điền khi tải trang đã tắt.", + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "Auto-fill on page load set to use default setting.", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "Tự động điền khi tải trang được đặt thành mặc định trong cài đặt.", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { - "message": "Turn off master password re-prompt to edit this field", + "message": "Tắt yêu cầu nhập lại mật khẩu chính để chỉnh sửa trường này", "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "Ẩn/hiện thanh điều hướng bên" }, "skipToContent": { - "message": "Skip to content" + "message": "Chuyển đến nội dung" }, "bitwardenOverlayButton": { - "message": "Bitwarden auto-fill menu button", + "message": "Nút menu tự động điền Bitwarden", "description": "Page title for the iframe containing the overlay button" }, "toggleBitwardenVaultOverlay": { - "message": "Toggle Bitwarden auto-fill menu", + "message": "Bật/tắt menu tự động điền Bitwarden", "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden auto-fill menu", + "message": "Menu tự động điền Bitwarden", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { - "message": "Unlock your account to view matching logins", + "message": "Mở khóa tài khoản của bạn để xem các thông tin đăng nhập phù hợp", + "description": "Text to display in overlay when the account is locked." + }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", "description": "Text to display in overlay when the account is locked." }, "unlockAccount": { - "message": "Unlock account", + "message": "Mở khóa tài khoản", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { - "message": "Fill credentials for", + "message": "Điền thông tin đăng nhập cho", "description": "Screen reader text for when overlay item is in focused" }, "partialUsername": { - "message": "Partial username", + "message": "Tên người dùng từng phần", "description": "Screen reader text for when a login item is focused where a partial username is displayed. SR will announce this phrase before reading the text of the partial username" }, "noItemsToShow": { - "message": "No items to show", + "message": "Không có mục nào để hiển thị", "description": "Text to show in overlay if there are no matching items" }, "newItem": { - "message": "New item", + "message": "Mục mới", "description": "Button text to display in overlay when there are no matching items" }, "addNewVaultItem": { - "message": "Add new vault item", + "message": "Thêm mục mới", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { - "message": "Bitwarden auto-fill menu available. Press the down arrow key to select.", + "message": "Danh sách tự động điền của Bitwarden sẵn sàng. Sử dụng phím mũi tên xuống để chọn.", "description": "Screen reader text for announcing when the overlay opens on the page" }, "turnOn": { - "message": "Turn on" + "message": "Bật" }, "ignore": { - "message": "Ignore" + "message": "Bỏ qua" }, "importData": { - "message": "Import data", + "message": "Nhập dữ liệu", "description": "Used for the header of the import dialog, the import button and within the file-password-prompt" }, "importError": { - "message": "Import error" + "message": "Lỗi nhập" }, "importErrorDesc": { - "message": "There was a problem with the data you tried to import. Please resolve the errors listed below in your source file and try again." + "message": "Có vấn đề với dữ liệu bạn cố gắng nhập. Vui lòng khắc phục các lỗi được liệt kê bên dưới trong tập tin nguồn của bạn và thử lại." }, "resolveTheErrorsBelowAndTryAgain": { - "message": "Resolve the errors below and try again." + "message": "Giải quyết các lỗi bên dưới và thử lại." }, "description": { - "message": "Description" + "message": "Mô tả" }, "importSuccess": { - "message": "Data successfully imported" + "message": "Dữ liệu đã được nhập thành công" }, "importSuccessNumberOfItems": { - "message": "A total of $AMOUNT$ items were imported.", + "message": "Đã nhập tổng cộng $AMOUNT$ mục.", "placeholders": { "amount": { "content": "$1", @@ -2974,46 +3335,46 @@ } }, "tryAgain": { - "message": "Try again" + "message": "Thử lại" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "Cần xác minh cho thao tác này. Hãy đặt mã PIN để tiếp tục." }, "setPin": { - "message": "Set PIN" + "message": "Thiết lập mã PIN" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Xác thực bằng sinh trắc học" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Đang chờ xác nhận" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "Không thể hoàn tất sinh trắc học." }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "Cần một phương pháp khác?" }, "useMasterPassword": { - "message": "Use master password" + "message": "Dùng mật khẩu chính" }, "usePin": { - "message": "Use PIN" + "message": "Dùng mã PIN" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Dùng sinh trắc học" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "Nhập mã xác minh được gửi đến email của bạn." }, "resendCode": { - "message": "Resend code" + "message": "Gửi lại mã" }, "total": { - "message": "Total" + "message": "Tổng" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "Bạn đang nhập dữ liệu vào $ORGANIZATION$. Dữ liệu của bạn có thể được chia sẻ với các thành viên của tổ chức này. Bạn có muốn tiếp tục không?", "placeholders": { "organization": { "content": "$1", @@ -3021,47 +3382,50 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Lỗi kết nối với dịch vụ Duo. Sử dụng phương thức đăng nhập hai bước khác hoặc liên hệ với Duo để được hỗ trợ." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "Khởi chạy Duo và làm theo các bước để hoàn tất đăng nhập." }, "duoRequiredForAccount": { - "message": "Duo two-step login is required for your account." + "message": "Tài khoản của bạn yêu cầu xác minh hai bước với Duo." }, "popoutTheExtensionToCompleteLogin": { - "message": "Popout the extension to complete login." + "message": "Bật tiện ích mở rộng để hoàn tất đăng nhập." }, "popoutExtension": { - "message": "Popout extension" + "message": "Tiện ích mở rộng dạng cửa sổ bật lên" }, "launchDuo": { - "message": "Launch Duo" + "message": "Khởi chạy Dou" }, "importFormatError": { - "message": "Data is not formatted correctly. Please check your import file and try again." + "message": "Dữ liệu không được định dạng đúng. Vui lòng kiểm tra tập tin nhập và thử lại." }, "importNothingError": { - "message": "Nothing was imported." + "message": "Không có gì được nhập." }, "importEncKeyError": { - "message": "Error decrypting the exported file. Your encryption key does not match the encryption key used export the data." + "message": "Lỗi giải mã tập tin đã xuất. Khóa mã hóa của bạn không khớp với khóa mã hóa được sử dụng để xuất dữ liệu." }, "invalidFilePassword": { - "message": "Invalid file password, please use the password you entered when you created the export file." + "message": "Mật khẩu tập tin không hợp lệ, vui lòng sử dụng mật khẩu bạn đã nhập khi xuất tập tin." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Đến" }, "learnAboutImportOptions": { - "message": "Learn about your import options" + "message": "Tìm hiểu các tuỳ chọn nhập của bạn" }, "selectImportFolder": { - "message": "Select a folder" + "message": "Chọn thư mục" }, "selectImportCollection": { - "message": "Select a collection" + "message": "Chọn bộ sưu tập" }, "importTargetHint": { - "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", + "message": "Chọn tùy chọn này để di chuyển nội dung tập tin đã được nhập đến $DESTINATION$", "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", "placeholders": { "destination": { @@ -3071,25 +3435,25 @@ } }, "importUnassignedItemsError": { - "message": "File contains unassigned items." + "message": "Tập tin chứa các mục không xác định." }, "selectFormat": { - "message": "Select the format of the import file" + "message": "Chọn định dạng tập tin nhập" }, "selectImportFile": { - "message": "Select the import file" + "message": "Chọn tập tin nhập" }, "chooseFile": { - "message": "Choose File" + "message": "Chọn tập tin" }, "noFileChosen": { - "message": "No file chosen" + "message": "Chưa chọn tập tin nào" }, "orCopyPasteFileContents": { - "message": "or copy/paste the import file contents" + "message": "hoặc sao chép/dán nội dung của tập tin nhập" }, "instructionsFor": { - "message": "$NAME$ Instructions", + "message": "Hướng dẫn dùng $NAME$", "description": "The title for the import tool instructions.", "placeholders": { "name": { @@ -3099,272 +3463,296 @@ } }, "confirmVaultImport": { - "message": "Confirm vault import" + "message": "Xác nhận nhập kho" }, "confirmVaultImportDesc": { - "message": "This file is password-protected. Please enter the file password to import data." + "message": "Tập tin này được bảo vệ bằng mật khẩu. Vui lòng nhập mật khẩu để nhập dữ liệu." }, "confirmFilePassword": { - "message": "Confirm file password" + "message": "Nhập lại mật khẩu tập tin" }, "exportSuccess": { - "message": "Vault data exported" + "message": "Đã xuất dữ liệu kho của bạn" }, "typePasskey": { - "message": "Passkey" + "message": "Mã khoá" }, "passkeyNotCopied": { - "message": "Passkey will not be copied" + "message": "Không thể sao chép mã khoá" }, "passkeyNotCopiedAlert": { - "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" + "message": "Bản sao sẽ không bao gồm mã khoá. Bạn có muốn tiếp tục tạo bản sao mục này?" }, "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { - "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." + "message": "Trang web yêu cầu xác minh. Tính năng này hiện chưa được hỗ trợ cho tài khoản không có mật khẩu chính." }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "Log in with passkey?" }, "passkeyAlreadyExists": { - "message": "A passkey already exists for this application." + "message": "Ứng dụng này đã có mã khoá." }, "noPasskeysFoundForThisApplication": { - "message": "No passkeys found for this application." + "message": "Không có mã khoá cho ứng dụng này." }, "noMatchingPasskeyLogin": { - "message": "You do not have a matching login for this site." + "message": "Bạn không có thông tin đăng nhập phù hợp cho trang web này." + }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" }, "confirm": { - "message": "Confirm" + "message": "Xác nhận" }, "savePasskey": { - "message": "Save passkey" + "message": "Lưu mã khoá" }, "savePasskeyNewLogin": { - "message": "Save passkey as new login" + "message": "Lưu mã khoá như đăng nhập mới" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" + }, "passkeyItem": { - "message": "Passkey Item" + "message": "Mục mã khoá" }, "overwritePasskey": { - "message": "Overwrite passkey?" + "message": "Ghi đè mã khoá?" }, "overwritePasskeyAlert": { - "message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?" + "message": "Mục này đã chứa mã khoá. Bạn có chắc muốn ghi đè mã khoá hiện tại không?" }, "featureNotSupported": { - "message": "Feature not yet supported" + "message": "Chưa hỗ trợ tính năng này" }, "yourPasskeyIsLocked": { - "message": "Authentication required to use passkey. Verify your identity to continue." + "message": "Yêu cầu xác thực để sử dụng mã khoá. Xác minh danh tính của bạn để tiếp tục." }, "multifactorAuthenticationCancelled": { - "message": "Multifactor authentication cancelled" + "message": "Đã hủy xác thực đa yếu tố" }, "noLastPassDataFound": { - "message": "No LastPass data found" + "message": "Không tìm thấy dữ liệu LastPass" }, "incorrectUsernameOrPassword": { - "message": "Incorrect username or password" + "message": "Tên người dùng hoặc mật khẩu không đúng" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "Mật khẩu không đúng" }, "incorrectCode": { - "message": "Incorrect code" + "message": "Mã không đúng" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "Mã PIN không đúng" }, "multifactorAuthenticationFailed": { - "message": "Multifactor authentication failed" + "message": "Xác thực đa yếu tố thất bại" }, "includeSharedFolders": { - "message": "Include shared folders" + "message": "Bao gồm các thư mục được chia sẻ" }, "lastPassEmail": { - "message": "LastPass Email" + "message": "Email LastPass" }, "importingYourAccount": { - "message": "Importing your account..." + "message": "Đang nhập tài khoản của bạn..." }, "lastPassMFARequired": { - "message": "LastPass multifactor authentication required" + "message": "Yêu cầu xác thực đa yếu tố LastPass" }, "lastPassMFADesc": { - "message": "Enter your one-time passcode from your authentication app" + "message": "Nhập mã OTP từ ứng dụng xác thực của bạn" }, "lastPassOOBDesc": { - "message": "Approve the login request in your authentication app or enter a one-time passcode." + "message": "Phê duyệt yêu cầu đăng nhập trên ứng dụng xác thực của bạn hoặc nhập mã OTP." }, "passcode": { - "message": "Passcode" + "message": "Mật mã" }, "lastPassMasterPassword": { - "message": "LastPass master password" + "message": "Mật khẩu chính LastPass" }, "lastPassAuthRequired": { - "message": "LastPass authentication required" + "message": "Yêu cầu xác thực LastPass" }, "awaitingSSO": { - "message": "Awaiting SSO authentication" + "message": "Đang chờ xác thực SSO" }, "awaitingSSODesc": { - "message": "Please continue to log in using your company credentials." + "message": "Vui lòng tiếp tục đăng nhập bằng thông tin đăng nhập của công ty bạn." }, "seeDetailedInstructions": { - "message": "See detailed instructions on our help site at", + "message": "Xem hướng dẫn chi tiết trên trang trợ giúp của chúng tôi tại", "description": "This is followed a by a hyperlink to the help website." }, "importDirectlyFromLastPass": { - "message": "Import directly from LastPass" + "message": "Nhập trực tiếp từ LastPass" }, "importFromCSV": { - "message": "Import from CSV" + "message": "Nhập từ CSV" }, "lastPassTryAgainCheckEmail": { - "message": "Try again or look for an email from LastPass to verify it's you." + "message": "Thử lại hoặc tìm email từ LastPass để xác minh đó là bạn." }, "collection": { - "message": "Collection" + "message": "Bộ Sưu Tập" }, "lastPassYubikeyDesc": { - "message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button." + "message": "Cắm khóa YubiKey được liên kết với tài khoản LastPass của bạn vào cổng USB của máy tính, sau đó nhấn nút trên YubiKey." }, "switchAccount": { - "message": "Switch account" + "message": "Chuyển tài khoản" }, "switchAccounts": { - "message": "Switch accounts" + "message": "Chuyển đổi tài khoản" }, "switchToAccount": { - "message": "Switch to account" + "message": "Chuyển sang tài khoản" }, "activeAccount": { - "message": "Active account" + "message": "Tài khoản đang hoạt động" }, "availableAccounts": { - "message": "Available accounts" + "message": "Các tài khoản khả dụng" }, "accountLimitReached": { - "message": "Account limit reached. Log out of an account to add another." + "message": "Số lượng tài khoản đã đạt giới hạn. Đăng xuất khỏi một tài khoản để thêm tài khoản khác." }, "active": { - "message": "active" + "message": "hoạt động" }, "locked": { - "message": "locked" + "message": "đã khóa" }, "unlocked": { - "message": "unlocked" + "message": "đã mở khóa" }, "server": { - "message": "server" + "message": "máy chủ" }, "hostedAt": { - "message": "hosted at" + "message": "được lưu trữ tại" }, "useDeviceOrHardwareKey": { - "message": "Use your device or hardware key" + "message": "Sử dụng thiết bị hoặc khóa phần cứng của bạn" }, "justOnce": { - "message": "Just once" + "message": "Chỉ một lần" }, "alwaysForThisSite": { - "message": "Always for this site" + "message": "Luôn cho trang này" }, "domainAddedToExcludedDomains": { - "message": "$DOMAIN$ added to excluded domains.", + "message": "$DOMAIN$ đã được thêm vào danh sách các tên miền loại trừ.", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, "commonImportFormats": { - "message": "Common formats", + "message": "Định dạng chung", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Tiếp tục tới Cài đặt trình duyệt?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Tiếp tục tới Trung tâm trợ giúp?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" + }, + "confirmContinueToHelpCenterPasswordManagementContent": { + "message": "Thay đổi cài đặt tự động điền và quản lý mật khẩu của trình duyệt của bạn.", + "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": "Bạn có thể xem và đặt các phím tắt của tiện ích mở rộng trong phần cài đặt trình duyệt.", + "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": "Thay đổi cài đặt tự động điền và quản lý mật khẩu của trình duyệt của bạn.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" + }, + "confirmContinueToBrowserKeyboardShortcutSettingsContent": { + "message": "Bạn có thể xem và thiết lập các phím tắt của tiện ích mở rộng trong phần cài đặt trình duyệt.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { - "message": "Make Bitwarden your default password manager?", + "message": "Đặt Bitwarden làm trình quản lý mật khẩu mặc định của bạn?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Bỏ qua tùy chọn này có thể gây ra xung đột giữa các đề xuất tự động điền của Bitwarden và trình duyệt của bạn.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { - "message": "Make Bitwarden your default password manager", + "message": "Bitwarden làm trình quản lý mật khẩu mặc định", "description": "Label for the setting that allows overriding the default browser autofill settings" }, "privacyPermissionAdditionNotGrantedTitle": { - "message": "Unable to set Bitwarden as the default password manager", + "message": "Không thể đặt Bitwarden làm trình quản lý mật khẩu mặc định", "description": "Title for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "privacyPermissionAdditionNotGrantedDescription": { - "message": "You must grant browser privacy permissions to Bitwarden to set it as the default password manager.", + "message": "Bạn phải cấp quyền riêng tư của trình duyệt cho Bitwarden để đặt nó làm trình quản lý mật khẩu mặc định.", "description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "makeDefault": { - "message": "Make default", + "message": "Đặt làm mặc định", "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { - "message": "Credentials saved successfully!", + "message": "Thông tin đăng nhập đã lưu thành công!", + "description": "Notification message for when saving credentials has succeeded." + }, + "passwordSaved": { + "message": "Password saved!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { - "message": "Credentials updated successfully!", + "message": "Thông tin đăng nhập đã được cập nhật thành công!", + "description": "Notification message for when updating credentials has succeeded." + }, + "passwordUpdated": { + "message": "Password updated!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { - "message": "Error saving credentials. Check console for details.", + "message": "Xảy ra lỗi trong quá trình lưu thông tin đăng nhập. Kiểm tra bảng điều khiển để biết thêm chi tiết.", "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "Thành công" }, "removePasskey": { - "message": "Remove passkey" + "message": "Xóa mã khoá" }, "passkeyRemoved": { - "message": "Passkey removed" - }, - "unassignedItemsBannerNotice": { - "message": "Lưu ý: Các mục tổ chức chưa được chỉ định sẽ không còn hiển thị trong chế độ xem Tất cả Vault và chỉ có thể truy cập được qua Bảng điều khiển dành cho quản trị viên." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Lưu ý: Vào ngày 16 tháng 5 năm 2024, các mục tổ chức chưa được chỉ định sẽ không còn hiển thị trong chế độ xem Tất cả Vault và sẽ chỉ có thể truy cập được qua Bảng điều khiển dành cho quản trị viên." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Gán các mục này vào một bộ sưu tập từ", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "để làm cho chúng hiển thị.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + "message": "Đã xóa mã khoá" }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Gợi ý điền tự động" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Lưu thông tin đăng nhập cho trang này để tự động điền" }, "yourVaultIsEmpty": { - "message": "Your vault is empty" + "message": "Kho của bạn trống" }, "noItemsMatchSearch": { - "message": "No items match your search" + "message": "Không có kết quả nào phù hợp với tìm kiếm của bạn" }, "clearFiltersOrTryAnother": { - "message": "Clear filters or try another search term" + "message": "Xóa bộ lọc hoặc thử từ khóa tìm kiếm khác" }, "copyInfoTitle": { - "message": "Copy info - $ITEMNAME$", + "message": "Sao chép thông tin - $ITEMNAME$", "description": "Title for a button that opens a menu with options to copy information from an item.", "placeholders": { "itemname": { @@ -3374,7 +3762,7 @@ } }, "copyNoteTitle": { - "message": "Copy Note - $ITEMNAME$", + "message": "Sao chép ghi chú - $ITEMNAME$", "description": "Title for a button copies a note to the clipboard.", "placeholders": { "itemname": { @@ -3384,7 +3772,7 @@ } }, "moreOptionsLabel": { - "message": "More options, $ITEMNAME$", + "message": "Thêm tùy chọn, $ITEMNAME$", "description": "Aria label for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -3394,7 +3782,7 @@ } }, "moreOptionsTitle": { - "message": "More options - $ITEMNAME$", + "message": "Thêm tùy chọn - $ITEMNAME$", "description": "Title for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -3404,7 +3792,7 @@ } }, "viewItemTitle": { - "message": "View item - $ITEMNAME$", + "message": "Xem mục - $ITEMNAME$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Tự động điền - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3424,40 +3812,40 @@ } }, "noValuesToCopy": { - "message": "No values to copy" + "message": "Không có giá trị để sao chép" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Gán vào bộ sưu tập" }, "copyEmail": { - "message": "Copy email" + "message": "Sao chép email" }, "copyPhone": { - "message": "Copy phone" + "message": "Sao chép số điện thoại" }, "copyAddress": { - "message": "Copy address" + "message": "Sao chép địa chỉ" }, "adminConsole": { "message": "Bảng điều khiển dành cho quản trị viên" }, "accountSecurity": { - "message": "Account security" + "message": "Bảo mật tài khoản" }, "notifications": { - "message": "Notifications" + "message": "Thông báo" }, "appearance": { - "message": "Appearance" + "message": "Giao diện" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Lỗi khi gán vào bộ sưu tập chỉ định." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Lỗi khi gán vào thư mục chỉ định." }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "Xem các mục trong $NAME$", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -3467,7 +3855,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "Quay lại $NAME$", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -3477,10 +3865,10 @@ } }, "new": { - "message": "New" + "message": "Mới" }, "removeItem": { - "message": "Remove $NAME$", + "message": "Xoá $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -3490,16 +3878,16 @@ } }, "itemsWithNoFolder": { - "message": "Items with no folder" + "message": "Các mục không được phân loại" }, "itemDetails": { - "message": "Item details" + "message": "Chi tiết mục" }, "itemName": { - "message": "Item name" + "message": "Tên mục" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "Bạn không thể xóa các bộ sưu tập với quyền chỉ xem: $COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3508,29 +3896,47 @@ } }, "organizationIsDeactivated": { - "message": "Organization is deactivated" + "message": "Tổ chức không còn hoạt động" }, "owner": { - "message": "Owner" + "message": "Chủ sở hữu" }, "selfOwnershipLabel": { - "message": "You", + "message": "Bạn", "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": "Không thể truy cập các mục trong tổ chức đã ngưng hoạt động. Hãy liên hệ với chủ sở hữu tổ chức để được hỗ trợ." + }, + "additionalInformation": { + "message": "Thông tin bổ sung" + }, + "itemHistory": { + "message": "Lịch sử mục" + }, + "lastEdited": { + "message": "Chỉnh sửa lần cuối" + }, + "ownerYou": { + "message": "Chủ sở hữu: Bạn" + }, + "linked": { + "message": "Đã liên kết" + }, + "copySuccessful": { + "message": "Sao chép thành công" }, "upload": { - "message": "Upload" + "message": "Tải lên" }, "addAttachment": { - "message": "Add attachment" + "message": "Thêm tệp đính kèm" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "Kích thước tối đa của tập tin là 500MB" }, "deleteAttachmentName": { - "message": "Delete attachment $NAME$", + "message": "Xoá tệp đính kèm $NAME$", "placeholders": { "name": { "content": "$1", @@ -3539,7 +3945,7 @@ } }, "downloadAttachmentName": { - "message": "Download $NAME$", + "message": "Tải xuống $NAME$", "placeholders": { "name": { "content": "$1", @@ -3548,15 +3954,389 @@ } }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Are you sure you want to permanently delete this attachment?" + "message": "Bạn có chắc chắn muốn xóa vĩnh viễn tệp đính kèm này không?" }, "premium": { - "message": "Premium" + "message": "Cao cấp" }, "freeOrgsCannotUseAttachments": { - "message": "Free organizations cannot use attachments" + "message": "Các tổ chức miễn phí không thể sử dụng tệp đính kèm" }, "filters": { - "message": "Filters" + "message": "Bộ lọc" + }, + "personalDetails": { + "message": "Thông tin cá nhân" + }, + "identification": { + "message": "ID" + }, + "contactInfo": { + "message": "Thông tin liên hệ" + }, + "downloadAttachment": { + "message": "Tải xuống - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Thông tin đăng nhập" + }, + "authenticatorKey": { + "message": "Khóa xác thực" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Thông tin thẻ" + }, + "cardBrandDetails": { + "message": "Chi tiết $BRAND$", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Thêm tài khoản" + }, + "loading": { + "message": "Đang tải" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Chỉ những thành viên của tổ chức có quyền truy cập vào các bộ sưu tập này mới có thể xem mục này." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Chỉ những thành viên của tổ chức có quyền truy cập vào các bộ sưu tập này mới có thể xem các mục này." + }, + "bulkCollectionAssignmentWarning": { + "message": "Bạn đã chọn $TOTAL_COUNT$ mục. Bạn không thể cập nhật $READONLY_COUNT$ mục vì bạn không có quyền chỉnh sửa.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Thêm trường" + }, + "add": { + "message": "Thêm" + }, + "fieldType": { + "message": "Loại trường" + }, + "fieldLabel": { + "message": "Tiêu đề trường" + }, + "textHelpText": { + "message": "Sử dụng các trường nhập liệu văn bản cho dữ liệu như câu hỏi bảo mật" + }, + "hiddenHelpText": { + "message": "Sử dụng các trường nhập liệu ẩn cho thông tin nhạy cảm như mật khẩu" + }, + "checkBoxHelpText": { + "message": "Dùng các ô tích chọn nếu bạn muốn tự động điền vào ô tích chọn của biểu mẫu, chẳng hạn như ghi nhớ email" + }, + "linkedHelpText": { + "message": "Sử dụng trường nhập liệu đã liên kết khi bạn gặp vấn đề với việc tự động điền trên một trang web cụ thể." + }, + "linkedLabelHelpText": { + "message": "Nhập thông tin định danh của trường như id, name, aria-label hoặc placeholder." + }, + "editField": { + "message": "Chỉnh sửa trường" + }, + "editFieldLabel": { + "message": "Chỉnh sửa $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Xoá $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "Đã thêm $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Sắp xếp lại $LABEL$. Sử dụng phím mũi tên để di chuyển mục lên hoặc xuống.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ đã di chuyển lên vị trí $INDEX$ / $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Chọn bộ sưu tập để gán" + }, + "personalItemTransferWarningSingular": { + "message": "1 mục sẽ được chuyển vĩnh viễn đến tổ chức đã chọn. Bạn sẽ không còn sở hữu mục này nữa." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ mục sẽ được chuyển vĩnh viễn đến tổ chức đã chọn. Bạn sẽ không còn sở hữu các mục này nữa.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 mục sẽ được chuyển vĩnh viễn đến $ORG$. Bạn sẽ không còn sở hữu mục này nữa.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ mục sẽ được chuyển vĩnh viễn đến $ORG$. Bạn sẽ không còn sở hữu các mục này nữa.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Đã gán vào bộ sưu tập thành công" + }, + "nothingSelected": { + "message": "Bạn chưa chọn gì." + }, + "movedItemsToOrg": { + "message": "Đã chuyển các mục được chọn đến $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Các mục đã được chuyển tới $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Mục đã được chuyển tới $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ đã di chuyển xuống vị trí $INDEX$ / $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Vị trí mục" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index f5a48ba127c..04db7f8f6ec 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -11,7 +11,10 @@ "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { - "message": "登录或者创建一个账户来访问您的安全密码库。" + "message": "登录或创建一个新账户以访问您的安全密码库。" + }, + "inviteAccepted": { + "message": "邀请已接受" }, "createAccount": { "message": "创建账户" @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "设置密码以完成账户的创建" }, - "login": { - "message": "登录" - }, "enterpriseSingleSignOn": { "message": "企业单点登录" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "主密码提示(可选)" }, + "joinOrganization": { + "message": "加入组织" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "通过设置主密码完成加入此组织。" + }, "tab": { "message": "标签页" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "复制安全码" }, + "copyName": { + "message": "复制名称" + }, + "copyCompany": { + "message": "复制公司信息" + }, + "copySSN": { + "message": "复制社会保障号码" + }, + "copyPassportNumber": { + "message": "复制护照号码" + }, + "copyLicenseNumber": { + "message": "复制许可证号码" + }, "autoFill": { "message": "自动填充" }, @@ -280,6 +301,24 @@ "editFolder": { "message": "编辑文件夹" }, + "newFolder": { + "message": "新增文件夹" + }, + "folderName": { + "message": "文件夹名称" + }, + "folderHintText": { + "message": "通过在父文件夹名后面跟随一个「/」来嵌套文件夹。例如:Social/Forums" + }, + "noFoldersAdded": { + "message": "未添加文件夹" + }, + "createFoldersToOrganize": { + "message": "创建文件夹以整理你的密码库项目" + }, + "deleteFolderPermanently": { + "message": "您确定要永久删除这个文件夹吗?" + }, "deleteFolder": { "message": "删除文件夹" }, @@ -321,7 +360,7 @@ "message": "自动生成安全可靠唯一的登录密码。" }, "bitWebVaultApp": { - "message": "Bitwarden 网页版应用" + "message": "Bitwarden 网页 App" }, "importItems": { "message": "导入项目" @@ -345,16 +384,56 @@ "message": "最小密码长度" }, "uppercase": { - "message": "大写 (A-Z)" + "message": "大写 (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "小写 (a-z)" + "message": "小写 (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "数字 (0-9)" + "message": "数字 (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "特殊字符 (!@#$%^&*)" + "message": "特殊字符 (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "包含", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "包含大写字符", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "包含小写字符", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "包含数字", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "包含特殊字符", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "单词个数" @@ -376,7 +455,12 @@ "message": "符号最少个数" }, "avoidAmbChar": { - "message": "避免易混淆的字符" + "message": "避免易混淆的字符", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "避免易混淆的字符", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "搜索密码库" @@ -556,6 +640,18 @@ "security": { "message": "安全" }, + "confirmMasterPassword": { + "message": "确认主密码" + }, + "masterPassword": { + "message": "主密码" + }, + "masterPassImportant": { + "message": "主密码忘记后,将无法恢复!" + }, + "masterPassHintLabel": { + "message": "主密码提示" + }, "errorOccurred": { "message": "发生了一个错误" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "您的新账户已创建!您现在可以登录了。" }, + "newAccountCreated2": { + "message": "您的新账户已成功创建!" + }, + "youHaveBeenLoggedIn": { + "message": "您已登录!" + }, "youSuccessfullyLoggedIn": { "message": "您已成功登录" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "必须填写验证码。" }, + "webauthnCancelOrTimeout": { + "message": "身份验证被取消或耗时过长。请重试。" + }, "invalidVerificationCode": { "message": "无效的验证码" }, @@ -613,7 +718,7 @@ } }, "autofillError": { - "message": "无法在此页面上自动填充所选项目。请改为手工复制并粘贴。" + "message": "无法在此页面上自动填充所选项目。请改为手动复制并粘贴。" }, "totpCaptureError": { "message": "无法从当前网页扫描二维码" @@ -624,6 +729,18 @@ "totpCapture": { "message": "从当前网页扫描验证器二维码" }, + "totpHelperTitle": { + "message": "无缝两步验证" + }, + "totpHelper": { + "message": "Bitwarden 可以存储并填充两步验证码。复制并粘贴密钥到此字段。" + }, + "totpHelperWithCapture": { + "message": "Bitwarden 可以存储并填充两步验证码。选择相机图标来截取此网站的验证器二维码,或者手动复制并粘贴密钥到此字段。" + }, + "learnMoreAboutAuthenticators": { + "message": "了解更多关于验证器的信息" + }, "copyTOTP": { "message": "复制验证器密钥 (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "您的登录会话已过期。" }, + "logIn": { + "message": "登录" + }, + "restartRegistration": { + "message": "重新开始注册" + }, + "expiredLink": { + "message": "失效链接" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "请重新注册或尝试登录。" + }, + "youMayAlreadyHaveAnAccount": { + "message": "您可能已经有一个账户了" + }, "logOutConfirmation": { "message": "确定要注销吗?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "新增 URI" }, + "addDomain": { + "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": { "message": "项目已添加" }, @@ -737,18 +873,27 @@ "enableAddLoginNotification": { "message": "询问添加登录" }, + "vaultSaveOptionsTitle": { + "message": "保存到密码库选项" + }, "addLoginNotificationDesc": { "message": "在密码库中找不到匹配项目时询问添加一个。" }, "addLoginNotificationDescAlt": { "message": "如果在密码库中找不到项目,询问添加一个。适用于所有已登录的账户。" }, + "showCardsInVaultView": { + "message": "在密码库视图中将支付卡显示为自动填充建议" + }, "showCardsCurrentTab": { "message": "在标签页上显示支付卡" }, "showCardsCurrentTabDesc": { "message": "在标签页上列出支付卡项目,以便于自动填充。" }, + "showIdentitiesInVaultView": { + "message": "在密码库视图中将身份显示为自动填充建议" + }, "showIdentitiesCurrentTab": { "message": "在标签页上显示身份" }, @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "默认 URI 匹配检测", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "选择在执行诸如自动填充之类的操作时对登录进行 URI 匹配检测的默认方式。" @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "1 GB 文件附件加密存储。" }, + "premiumSignUpEmergency": { + "message": "紧急访问。" + }, "premiumSignUpTwoStepOptions": { "message": "专有的两步登录选项,如 YubiKey 和 Duo。" }, @@ -998,7 +1146,7 @@ "message": "优先客户支持。" }, "ppremiumSignUpFuture": { - "message": "未来会增加更多高级功能。敬请期待!" + "message": "未来的更多高级功能。敬请期待!" }, "premiumPurchase": { "message": "购买高级版" @@ -1006,14 +1154,29 @@ "premiumPurchaseAlert": { "message": "您可以在 bitwarden.com 网页版密码库购买高级会员。现在要访问吗?" }, + "premiumPurchaseAlertV2": { + "message": "您可以在 Bitwarden 网页 App 的账户设置中购买高级版。" + }, "premiumCurrentMember": { "message": "您目前是高级会员!" }, "premiumCurrentMemberThanks": { "message": "感谢您支持 Bitwarden。" }, + "premiumFeatures": { + "message": "升级到高级版并接收:" + }, "premiumPrice": { - "message": "只需 $PRICE$ /年!", + "message": "全部仅需 $PRICE$ /年!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, + "premiumPriceV2": { + "message": "全部仅需 $PRICE$ 每年!", "placeholders": { "price": { "content": "$1", @@ -1028,7 +1191,7 @@ "message": "自动复制 TOTP" }, "disableAutoTotpCopyDesc": { - "message": "如果登录包含验证器密钥,当自动填充此登录时,TOTP 验证码将复制到剪贴板。" + "message": "如果登录包含验证器密钥,当自动填充此登录时,将 TOTP 验证码复制到剪贴板。" }, "enableAutoBiometricsPrompt": { "message": "启动时要求生物识别" @@ -1179,13 +1342,22 @@ }, "showAutoFillMenuOnFormFields": { "message": "在表单字段上显示自动填充菜单", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { + "autofillSuggestionsSectionTitle": { + "message": "自动填充建议" + }, + "showInlineMenuLabel": { + "message": "在表单字段中显示自动填充建议" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "选择图标时显示建议" + }, + "showInlineMenuOnFormFieldsDescAlt": { "message": "适用于所有已登录的账户。" }, "turnOffBrowserBuiltInPasswordManagerSettings": { - "message": "关闭您浏览器自带的密码管理器设置以避免冲突。" + "message": "关闭您浏览器内置的密码管理器设置以避免冲突。" }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { "message": "编辑浏览器设置。" @@ -1202,14 +1374,33 @@ "message": "选中自动填充图标时", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "页面加载时自动填充" + }, "enableAutoFillOnPageLoad": { "message": "页面加载时自动填充" }, "enableAutoFillOnPageLoadDesc": { "message": "网页加载时如果检测到登录表单,则执行自动填充。" }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$警告:$CLOSETAG$不完整或不信任的网站可以利用页面加载时的自动填充功能。", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { - "message": "不完整或不信任的网站可以在页面加载时自动填充。" + "message": "不完整或不信任的网站可以利用页面加载时的自动填充功能。" + }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "了解更多关于风险的信息" }, "learnMoreAboutAutofill": { "message": "了解更多关于自动填充的信息" @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "在侧边栏中打开密码库" }, - "commandAutofillDesc": { - "message": "为当前网站自动填充最后使用的登录信息" + "commandAutofillLoginDesc": { + "message": "为当前网站自动填充最后一次使用的登录信息" + }, + "commandAutofillCardDesc": { + "message": "为当前网站自动填充最后一次使用的支付卡信息" + }, + "commandAutofillIdentityDesc": { + "message": "为当前网站自动填充最后一次使用的身份信息" }, "commandGeneratePasswordDesc": { "message": "生成一个新的随机密码并将其复制到剪贴板中。" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "布尔型" }, + "cfTypeCheckbox": { + "message": "复选框型" + }, "cfTypeLinked": { "message": "链接型", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1397,7 +1597,7 @@ "message": "公司" }, "ssn": { - "message": "社会保险号码" + "message": "社会保障号码" }, "passportNumber": { "message": "护照号码" @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "查看 $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "密码历史记录" }, @@ -1533,6 +1742,10 @@ "message": "基础域", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "基础域(推荐)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "域名", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "匹配检测", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "默认匹配检测", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "切换选项" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "没有可列出的密码。" }, + "clearHistory": { + "message": "清除历史记录" + }, + "noPasswordsToShow": { + "message": "没有可显示的密码" + }, + "noRecentlyGeneratedPassword": { + "message": "您最近还没有生成过密码" + }, "remove": { "message": "移除" }, @@ -1677,6 +1899,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": "密码库超时动作" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "您的新主密码不符合策略要求。" }, - "receiveMarketingEmails": { - "message": "接收来自 Bitwarden 的电子邮件,以获取公告、建议和调研。" + "receiveMarketingEmailsV2": { + "message": "获取来自 Bitwarden 的建议、公告和调研电子邮件。" }, "unsubscribe": { "message": "取消订阅" @@ -1874,6 +2119,12 @@ "nativeMessagingWrongUserTitle": { "message": "账户不匹配" }, + "nativeMessagingWrongUserKeyDesc": { + "message": "生物识别解锁失败。生物识别安全钥匙解锁密码库失败。请尝试重新设置生物识别。" + }, + "nativeMessagingWrongUserKeyTitle": { + "message": "生物识别密钥不匹配" + }, "biometricsNotEnabledTitle": { "message": "生物识别未设置" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "请在桌面应用程序中解锁此用户,然后重试。" }, + "biometricsNotAvailableTitle": { + "message": "生物识别解锁不可用" + }, + "biometricsNotAvailableDesc": { + "message": "生物识别解锁当前不可用。请稍后再试。" + }, "biometricsFailedTitle": { "message": "生物识别失败" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "组织策略已阻止将项目导入您的个人密码库。" }, + "domainsTitle": { + "message": "域名", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "排除域名" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "Bitwarden 不会询问保存所有已登录的账户的这些域名的登录信息。必须刷新页面才能使更改生效。" }, + "websiteItemLabel": { + "message": "网站 $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ 不是一个有效的域名", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "排除域名更改已保存" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "密码保护" }, + "copyLink": { + "message": "复制链接" + }, "copySendLink": { "message": "复制 Send 链接", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send 已创建", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send 创建成功!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "在接下来的 $DAYS$ 天内,任何拥有链接的人都可以访问此 Send。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send 链接已复制", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send 已保存", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "需要验证电子邮件" }, + "emailVerifiedV2": { + "message": "电子邮箱已验证" + }, "emailVerificationRequiredDesc": { "message": "您必须验证电子邮件才能使用此功能。您可以在网页密码库中验证您的电子邮件。" }, @@ -2179,6 +2476,9 @@ "updateWeakMasterPasswordWarning": { "message": "您的主密码不符合某一项或多项组织策略要求。要访问密码库,必须立即更新您的主密码。继续操作将使您退出当前会话,并要求您重新登录。其他设备上的活动会话可能会继续保持活动状态长达一小时。" }, + "tdeDisabledMasterPasswordRequired": { + "message": "您的组织禁用了信任设备加密。要访问您的密码库,请设置一个主密码。" + }, "resetPasswordPolicyAutoEnroll": { "message": "自动注册" }, @@ -2295,7 +2595,7 @@ "message": "您的会话已超时。请返回然后尝试重新登录。" }, "exportingPersonalVaultTitle": { - "message": "导出个人密码库" + "message": "正在导出个人密码库" }, "exportingIndividualVaultDescription": { "message": "仅会导出与 $EMAIL$ 关联的个人密码库项目,不包括组织密码库项目。仅会导出密码库项目信息,不包括关联的附件。", @@ -2555,7 +2855,7 @@ "message": "使用设备登录" }, "loginWithDeviceEnabledInfo": { - "message": "设备登录必须在 Bitwarden 应用程序的设置中启用。需要其他登录选项吗?" + "message": "必须在 Bitwarden App 的设置中启用设备登录。需要其他登录选项吗?" }, "fingerprintPhraseHeader": { "message": "指纹短语" @@ -2606,7 +2906,7 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "您的组织策略已开启在页面加载时的自动填充。" + "message": "您的组织策略已开启页面加载时自动填充。" }, "howToAutofill": { "message": "如何自动填充" @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "自动填充设置" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "自动填充快捷键" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "更改快捷键" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "管理快捷键" + }, "autofillShortcut": { "message": "自动填充键盘快捷键" }, - "autofillShortcutNotSet": { - "message": "未设置自动填充快捷键。可在浏览器的设置中更改它。" + "autofillLoginShortcutNotSet": { + "message": "未设置自动填充登录快捷键。请在浏览器设置中更改它。" }, - "autofillShortcutText": { - "message": "自动填充快捷键为:$COMMAND$。可在浏览器的设置中更改它。", + "autofillLoginShortcutText": { + "message": "自动填充登录快捷键是 $COMMAND$。请在浏览器设置中管理所有快捷键。", "placeholders": { "command": { "content": "$1", @@ -2645,7 +2954,7 @@ } }, "autofillShortcutTextSafari": { - "message": "默认自动填充快捷方式:$COMMAND$。", + "message": "默认的自动填充快捷键:$COMMAND$", "placeholders": { "command": { "content": "$1", @@ -2681,7 +2990,7 @@ "message": "必须填写组织 SSO 标识符。" }, "creatingAccountOn": { - "message": "创建账户于" + "message": "正创建账户于" }, "checkYourEmail": { "message": "检查您的电子邮箱" @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "设备已信任" }, + "sendsNoItemsTitle": { + "message": "没有活跃的 Send", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "使用 Send 与任何人安全地分享加密信息。", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "必须输入内容。" }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "有 1 个字段需要您注意。" + }, + "multipleFieldsNeedAttention": { + "message": "有 $COUNT$ 个字段需要您注意。", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- 选择 --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "具有主密码重新提示的项目无法在页面加载时自动填充。页面加载时的自动填充功能已关闭。", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { - "message": "页面加载时自动填充设置为默认设置。", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "message": "页面加载时自动填充设置为使用默认设置。", + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "关闭主密码重新提示以编辑此字段", @@ -2911,10 +3240,18 @@ "message": "解锁您的账户以查看匹配的登录", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "解锁您的账户以查看自动填充建议", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "解锁账户", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "解锁您的账户(在新窗口中打开)", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "为其填写凭据", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "添加新的密码库项目", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "新增登录", + "description": "Button text to display within inline menu when there are no matching items on a login field" + }, + "addNewLoginItemAria": { + "message": "添加新的密码库登录项目(在新窗口中打开)", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "新增支付卡", + "description": "Button text to display within inline menu when there are no matching items on a credit card field" + }, + "addNewCardItemAria": { + "message": "添加新的密码库支付卡项目(在新窗口中打开)", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "新增身份", + "description": "Button text to display within inline menu when there are no matching items on an identity field" + }, + "addNewIdentityItemAria": { + "message": "添加新的密码库身份项目(在新窗口中打开)", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Bitwarden 自动填充菜单可用。按向下箭头键进行选择。", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -2974,7 +3335,7 @@ } }, "tryAgain": { - "message": "再试一次" + "message": "请重试" }, "verificationRequiredForActionSetPinToContinue": { "message": "此操作需要验证。设置一个 PIN 码以继续。" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "与 Duo 服务连接时出错。使用不同的两步登录方式或联系 Duo 寻求帮助。" + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "启动 DUO 并按照步骤完成登录。" }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "无效的文件密码,请使用您创建导出文件时输入的密码。" }, - "importDestination": { - "message": "导入目的地" + "destination": { + "message": "目的地" }, "learnAboutImportOptions": { "message": "了解您的导入选项" @@ -3122,7 +3486,7 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "启动站点需要验证。对于没有主密码的账户,此功能尚未实现。" }, - "logInWithPasskey": { + "logInWithPasskeyQuestion": { "message": "使用通行密钥登录吗?" }, "passkeyAlreadyExists": { @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "不存在匹配此站点的登录项目。" }, + "noMatchingLoginsForSite": { + "message": "此站点没有匹配的登录项目" + }, "confirm": { "message": "确认" }, @@ -3143,9 +3510,12 @@ "savePasskeyNewLogin": { "message": "作为新的登录项目保存通行密钥" }, - "choosePasskey": { + "chooseCipherForPasskeySave": { "message": "选择一个用于保存此通行密钥的登录项目" }, + "chooseCipherForPasskeyAuth": { + "message": "选择一个用于登录的通行密钥" + }, "passkeyItem": { "message": "通行密钥项目" }, @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "常规格式", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "前往浏览器设置吗?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "前往帮助中心吗?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" + }, + "confirmContinueToHelpCenterPasswordManagementContent": { + "message": "更改浏览器的自动填充和密码管理设置。", + "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": "您可以在浏览器的设置中查看和设置扩展的快捷键。", + "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": "更改浏览器的自动填充和密码管理设置。", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" + }, + "confirmContinueToBrowserKeyboardShortcutSettingsContent": { + "message": "您可以在浏览器的设置中查看和设置扩展的快捷键。", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "将 Bitwarden 设置为您的默认密码管理器吗?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "忽略此选项可能会导致 Bitwarden 自动填充菜单与浏览器自带功能产生冲突。", + "message": "忽略此选项可能会导致 Bitwarden 自动填充建议与浏览器产生冲突。", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "凭据保存成功!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "密码已保存!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "凭据更新成功!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "密码已更新!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "保存凭据时出错。检查控制台以获取详细信息。", "description": "Notification message for when saving credentials has failed." @@ -3334,20 +3736,6 @@ "passkeyRemoved": { "message": "通行密钥已移除" }, - "unassignedItemsBannerNotice": { - "message": "注意:未分配的组织项目在「所有密码库」视图中不再可见,只能通过管理控制台访问。" - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "注意:从 2024 年 5 月 16 日起,未分配的组织项目在「所有密码库」视图中将不再可见,只能通过管理控制台访问。" - }, - "unassignedItemsBannerCTAPartOne": { - "message": "将这些项目分配到集合,通过", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": ",以使其可见。", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { "message": "自动填充建议" }, @@ -3415,7 +3803,7 @@ }, "autofillTitle": { "message": "自动填充 - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "没有要复制的值" }, - "assignCollections": { - "message": "分配集合" + "assignToCollections": { + "message": "分配到集合" }, "copyEmail": { "message": "复制电子邮件地址" @@ -3493,13 +3881,13 @@ "message": "无文件夹的项目" }, "itemDetails": { - "message": "Item details" + "message": "项目详情" }, "itemName": { - "message": "Item name" + "message": "项目名称" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "您无法删除仅具有「查看」权限的集合:$COLLECTIONS$", "placeholders": { "collections": { "content": "$1", @@ -3511,15 +3899,33 @@ "message": "组织已停用" }, "owner": { - "message": "Owner" + "message": "所有者" }, "selfOwnershipLabel": { - "message": "You", + "message": "您", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { "message": "无法访问已停用组织中的项目。请联系您的组织所有者获取协助。" }, + "additionalInformation": { + "message": "更多信息" + }, + "itemHistory": { + "message": "项目历史记录" + }, + "lastEdited": { + "message": "上次编辑" + }, + "ownerYou": { + "message": "所有者:您" + }, + "linked": { + "message": "已链接" + }, + "copySuccessful": { + "message": "复制成功" + }, "upload": { "message": "上传" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "筛选" + }, + "personalDetails": { + "message": "个人信息" + }, + "identification": { + "message": "身份" + }, + "contactInfo": { + "message": "联系信息" + }, + "downloadAttachment": { + "message": "下载 - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "卡号结尾为", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "登录凭据" + }, + "authenticatorKey": { + "message": "验证器密钥" + }, + "autofillOptions": { + "message": "自动填充选项" + }, + "websiteUri": { + "message": "网站 (URI)" + }, + "websiteUriCount": { + "message": "网站 (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": "网址已添加" + }, + "addWebsite": { + "message": "添加网站" + }, + "deleteWebsite": { + "message": "删除网站" + }, + "defaultLabel": { + "message": "默认 ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "显示匹配检测 $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "隐藏匹配检测 $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "页面加载时自动填充吗?" + }, + "cardExpiredTitle": { + "message": "过期的支付卡" + }, + "cardExpiredMessage": { + "message": "如果您的支付卡已续期,请更新该卡的信息。" + }, + "cardDetails": { + "message": "支付卡详情" + }, + "cardBrandDetails": { + "message": "$BRAND$ 详情", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "启用动画" + }, + "addAccount": { + "message": "添加账户" + }, + "loading": { + "message": "加载中" + }, + "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": "分配" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "只有具有这些集合访问权限的组织成员才能看到这个项目。" + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "只有具有这些集合访问权限的组织成员才能看到这些项目。" + }, + "bulkCollectionAssignmentWarning": { + "message": "您选择了 $TOTAL_COUNT$ 个项目。其中的 $READONLY_COUNT$ 个项目由于您没有编辑权限,您将无法更新它们。", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "添加字段" + }, + "add": { + "message": "添加" + }, + "fieldType": { + "message": "字段类型" + }, + "fieldLabel": { + "message": "字段标签" + }, + "textHelpText": { + "message": "对于如安全问题之类的数据,请使用文本型字段" + }, + "hiddenHelpText": { + "message": "对于如密码之类的敏感数据,请使用隐藏型字段" + }, + "checkBoxHelpText": { + "message": "如果您想自动勾选表单复选框(例如记住电子邮件地址),请使用复选框" + }, + "linkedHelpText": { + "message": "当您处理特定网站的自动填充问题时,请使用链接型字段。" + }, + "linkedLabelHelpText": { + "message": "输入字段的 html id、名称、aria-label 或占位符。" + }, + "editField": { + "message": "编辑字段" + }, + "editFieldLabel": { + "message": "编辑 $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "删除 $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ 已添加", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "重新排序 $LABEL$。使用方向键向上或向下移动项目。", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ 已上移,位置 $INDEX$ / $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "选择要分配的集合" + }, + "personalItemTransferWarningSingular": { + "message": "1 个项目将永久转移到所选组织。您将不再拥有该项目。" + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ 个项目将永久转移到所选组织。您将不再拥有这些项目。", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 个项目将永久转移到 $ORG$ 。您将不再拥有该项目。", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ 个项目将永久转移到 $ORG$ 。您将不再拥有这些项目。", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "成功分配了集合" + }, + "nothingSelected": { + "message": "您尚未选择任何内容。" + }, + "movedItemsToOrg": { + "message": "所选项目已移动到 $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "项目已移动到 $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "项目已移动到 $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ 已下移,位置 $INDEX$ / $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "项目位置" + }, + "fileSends": { + "message": "文件 Send" + }, + "textSends": { + "message": "文本 Send" + }, + "bitwardenNewLook": { + "message": "Bitwarden 拥有一个新的外观!" + }, + "bitwardenNewLookDesc": { + "message": "从密码库标签页自动填充和搜索比以往任何时候都更简单直观。来看看吧!" + }, + "accountActions": { + "message": "账户操作" + }, + "showNumberOfAutofillSuggestions": { + "message": "在扩展图标上显示自动填充建议的登录的数量" + }, + "systemDefault": { + "message": "跟随系统" + }, + "enterprisePolicyRequirementsApplied": { + "message": "企业策略要求已应用于此设置" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "显示字符计数" + }, + "hideCharacterCount": { + "message": "隐藏字符计数" + }, + "itemsInTrash": { + "message": "回收站中的项目" + }, + "noItemsInTrash": { + "message": "回收站中没有项目" + }, + "noItemsInTrashDesc": { + "message": "您删除的项目将显示在这里,并在 30 天后永久删除" + }, + "trashWarning": { + "message": "回收站中超过 30 天的项目将被自动删除" + }, + "restore": { + "message": "恢复" + }, + "deleteForever": { + "message": "永久删除" + }, + "noEditPermissions": { + "message": "您没有编辑此项目的权限" } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index f30c79296ef..d61e831d738 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -13,6 +13,9 @@ "loginOrCreateNewAccount": { "message": "登入或建立帳戶以存取您的安全密碼庫。" }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "createAccount": { "message": "建立帳戶" }, @@ -22,9 +25,6 @@ "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" }, - "login": { - "message": "登入" - }, "enterpriseSingleSignOn": { "message": "企業單一登入" }, @@ -68,6 +68,12 @@ "masterPassHint": { "message": "主密碼提示(選用)" }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "tab": { "message": "分頁" }, @@ -107,6 +113,21 @@ "copySecurityCode": { "message": "複製安全代碼" }, + "copyName": { + "message": "Copy name" + }, + "copyCompany": { + "message": "Copy company" + }, + "copySSN": { + "message": "Copy Social Security number" + }, + "copyPassportNumber": { + "message": "Copy passport number" + }, + "copyLicenseNumber": { + "message": "Copy license number" + }, "autoFill": { "message": "自動填入" }, @@ -150,7 +171,7 @@ "message": "登入您的密碼庫" }, "autoFillInfo": { - "message": "沒有可以自動填入目前瀏覽器分頁的登入資料。" + "message": "沒有可以自動填入目前瀏覽器分頁的登入資訊。" }, "addLogin": { "message": "新增登入資料" @@ -280,6 +301,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,16 +384,56 @@ "message": "最小密碼長度" }, "uppercase": { - "message": "大寫 (A-Z)" + "message": "大寫 (A-Z)", + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { - "message": "小寫 (a-z)" + "message": "小寫 (a-z)", + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "數字 (0-9)" + "message": "數字 (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { - "message": "特殊字元 (!@#$%^&*)" + "message": "特殊字元 (!@#$%^&*)", + "description": "deprecated. Use specialCharactersLabel instead." + }, + "include": { + "message": "Include", + "description": "Card header for password generator include block" + }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" }, "numWords": { "message": "單字數量" @@ -376,7 +455,12 @@ "message": "最少符號位數" }, "avoidAmbChar": { - "message": "避免易混淆的字元" + "message": "避免易混淆的字元", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "searchVault": { "message": "搜尋密碼庫" @@ -457,10 +541,10 @@ "message": "Unlock options" }, "unlockMethodNeededToChangeTimeoutActionDesc": { - "message": "設定一個解鎖方式來變更您的密碼庫逾時動作。" + "message": "設定解鎖方法來變更您的密碼庫逾時動作。" }, "unlockMethodNeeded": { - "message": "設定中設定解鎖" + "message": "在設定中設定一個解鎖方式" }, "sessionTimeoutHeader": { "message": "Session timeout" @@ -556,6 +640,18 @@ "security": { "message": "安全" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "發生錯誤" }, @@ -587,6 +683,12 @@ "newAccountCreated": { "message": "帳戶已建立!現在可以登入了。" }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "youSuccessfullyLoggedIn": { "message": "登入成功" }, @@ -599,6 +701,9 @@ "verificationCodeRequired": { "message": "必須填入驗證碼。" }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "無效的驗證碼" }, @@ -613,7 +718,7 @@ } }, "autofillError": { - "message": "無法在此頁面自動填入所選項目。請手動複製貼上。" + "message": "無法在此頁面自動填入所選項目。請手動複製並貼上。" }, "totpCaptureError": { "message": "無法掃描此網頁的二維碼" @@ -624,6 +729,18 @@ "totpCapture": { "message": "從目前網頁掃描驗證器二維碼" }, + "totpHelperTitle": { + "message": "Make 2-step verification seamless" + }, + "totpHelper": { + "message": "Bitwarden can store and fill 2-step verification codes. Copy and paste the key into this field." + }, + "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": "複製驗證器金鑰 (TOTP)" }, @@ -636,6 +753,21 @@ "loginExpired": { "message": "您的登入階段已過期。" }, + "logIn": { + "message": "Log in" + }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "您確定要登出嗎?" }, @@ -697,6 +829,10 @@ "newUri": { "message": "新增 URI" }, + "addDomain": { + "message": "Add domain", + "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": { "message": "項目已新增" }, @@ -737,17 +873,26 @@ "enableAddLoginNotification": { "message": "詢問新增登入資料" }, + "vaultSaveOptionsTitle": { + "message": "Save to vault options" + }, "addLoginNotificationDesc": { "message": "在密碼庫中找不到相符的項目時詢問是否新增項目。" }, "addLoginNotificationDescAlt": { "message": "如果在您的密碼庫中找不到項目,則詢問是否新增項目。適用於所有已登入的帳戶。" }, + "showCardsInVaultView": { + "message": "Show cards as Autofill suggestions on Vault view" + }, "showCardsCurrentTab": { "message": "於分頁頁面顯示支付卡" }, "showCardsCurrentTabDesc": { - "message": "於分頁頁面顯示支付卡以便於自動填入。" + "message": "於分頁頁面顯示信用卡以便於自動填入。" + }, + "showIdentitiesInVaultView": { + "message": "Show identifies as Autofill suggestions on Vault view" }, "showIdentitiesCurrentTab": { "message": "於分頁頁面顯示身分" @@ -810,7 +955,7 @@ }, "defaultUriMatchDetection": { "message": "預設的 URI 一致性偵測", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "defaultUriMatchDetectionDesc": { "message": "選擇在執行自動填入等動作時對登入資料進行 URI 一致性偵測的預設方式。" @@ -985,6 +1130,9 @@ "ppremiumSignUpStorage": { "message": "用於檔案附件的 1 GB 加密儲存空間。" }, + "premiumSignUpEmergency": { + "message": "Emergency access." + }, "premiumSignUpTwoStepOptions": { "message": "專有的兩步驟登入選項,例如 YubiKey 和 Duo。" }, @@ -1006,12 +1154,18 @@ "premiumPurchaseAlert": { "message": "您可以在 bitwarden.com 網頁版密碼庫購買進階會員資格。現在要前往嗎?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "您目前為進階會員!" }, "premiumCurrentMemberThanks": { "message": "感謝您支持 Bitwarden 。" }, + "premiumFeatures": { + "message": "Upgrade to Premium and receive:" + }, "premiumPrice": { "message": "每年只需 $PRICE$!", "placeholders": { @@ -1021,6 +1175,15 @@ } } }, + "premiumPriceV2": { + "message": "All for just $PRICE$ per year!", + "placeholders": { + "price": { + "content": "$1", + "example": "$10" + } + } + }, "refreshComplete": { "message": "狀態更新完成" }, @@ -1179,10 +1342,19 @@ }, "showAutoFillMenuOnFormFields": { "message": "在表單欄位上顯示自動填入選單", - "description": "Represents the message for allowing the user to enable the auto-fill overlay" + "description": "Represents the message for allowing the user to enable the autofill overlay" }, - "showAutoFillMenuOnFormFieldsDescAlt": { - "message": "適用於所有已登入的帳戶。" + "autofillSuggestionsSectionTitle": { + "message": "Autofill suggestions" + }, + "showInlineMenuLabel": { + "message": "Show autofill suggestions on form fields" + }, + "showInlineMenuOnIconSelectionLabel": { + "message": "Display suggestions when icon is selected" + }, + "showInlineMenuOnFormFieldsDescAlt": { + "message": "Applies to all logged in accounts." }, "turnOffBrowserBuiltInPasswordManagerSettings": { "message": "關閉你的瀏覽器內建密碼管理器設定以避免衝突。" @@ -1202,15 +1374,34 @@ "message": "當選取自動填入圖示時", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, + "enableAutoFillOnPageLoadSectionTitle": { + "message": "Autofill on page load" + }, "enableAutoFillOnPageLoad": { "message": "頁面載入時自動填入" }, "enableAutoFillOnPageLoadDesc": { "message": "網頁載入時如果偵測到登入表單,則執行自動填入。" }, + "autofillOnPageLoadWarning": { + "message": "$OPENTAG$Warning:$CLOSETAG$ Compromised or untrusted websites can exploit autofill on page load.", + "placeholders": { + "openTag": { + "content": "$1", + "example": "" + }, + "closeTag": { + "content": "$2", + "example": "" + } + } + }, "experimentalFeature": { "message": "被入侵或不可信任的網站可不當利用頁面載入時的自動填入功能。" }, + "learnMoreAboutAutofillOnPageLoadLinkText": { + "message": "Learn more about risks" + }, "learnMoreAboutAutofill": { "message": "進一步瞭解「自動填入」功能" }, @@ -1238,8 +1429,14 @@ "commandOpenSidebar": { "message": "在側邊欄中開啟密碼庫" }, - "commandAutofillDesc": { - "message": "自動將上次使用的登入資料填入目前網站" + "commandAutofillLoginDesc": { + "message": "Autofill the last used login for the current website" + }, + "commandAutofillCardDesc": { + "message": "Autofill the last used card for the current website" + }, + "commandAutofillIdentityDesc": { + "message": "Autofill the last used identity for the current website" }, "commandGeneratePasswordDesc": { "message": "產生一組新的隨機密碼並將它複製到剪貼簿中。" @@ -1271,6 +1468,9 @@ "cfTypeBoolean": { "message": "布林值" }, + "cfTypeCheckbox": { + "message": "Checkbox" + }, "cfTypeLinked": { "message": "連結型", "description": "This describes a field that is 'linked' (tied) to another field." @@ -1471,6 +1671,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "密碼歷史記錄" }, @@ -1533,6 +1742,10 @@ "message": "基底網域", "description": "Domain name. Ex. website.com" }, + "baseDomainOptionRecommended": { + "message": "Base domain (recommended)", + "description": "Domain name. Ex. website.com" + }, "domainName": { "message": "網域名稱", "description": "Domain name. Ex. website.com" @@ -1553,11 +1766,11 @@ }, "matchDetection": { "message": "一致性偵測", - "description": "URI match detection for auto-fill." + "description": "URI match detection for autofill." }, "defaultMatchDetection": { "message": "預設一致性偵測", - "description": "Default URI match detection for auto-fill." + "description": "Default URI match detection for autofill." }, "toggleOptions": { "message": "切換選項" @@ -1583,6 +1796,15 @@ "noPasswordsInList": { "message": "沒有可列出的密碼。" }, + "clearHistory": { + "message": "Clear history" + }, + "noPasswordsToShow": { + "message": "No passwords to show" + }, + "noRecentlyGeneratedPassword": { + "message": "You haven't generated a password recently" + }, "remove": { "message": "移除" }, @@ -1677,6 +1899,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": "密碼庫逾時動作" }, @@ -1799,8 +2044,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "您新的主密碼不符合原則要求。" }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1874,6 +2119,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": "生物特徵辨識未設定" }, @@ -1892,6 +2143,12 @@ "biometricsNotUnlockedDesc": { "message": "Please unlock this user in the desktop application and try again." }, + "biometricsNotAvailableTitle": { + "message": "Biometric unlock unavailable" + }, + "biometricsNotAvailableDesc": { + "message": "Biometric unlock is currently unavailable. Please try again later." + }, "biometricsFailedTitle": { "message": "生物特徵辨識失敗" }, @@ -1919,6 +2176,10 @@ "personalOwnershipPolicyInEffectImports": { "message": "某個組織原則已禁止您將項目匯入至您的個人密碼庫。" }, + "domainsTitle": { + "message": "Domains", + "description": "A category title describing the concept of web domains" + }, "excludedDomains": { "message": "排除網域" }, @@ -1928,15 +2189,27 @@ "excludedDomainsDescAlt": { "message": "對於所有已登入的帳戶,Bitwarden 不會詢問是否儲存這些網域的登入資訊。您必須重新整理頁面以使變更生效。" }, + "websiteItemLabel": { + "message": "Website $number$ (URI)", + "placeholders": { + "number": { + "content": "$1", + "example": "3" + } + } + }, "excludedDomainsInvalidDomain": { "message": "$DOMAIN$ 不是一個有效的網域", "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, + "excludedDomainsSavedSuccess": { + "message": "Excluded domain changes saved" + }, "send": { "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -1972,6 +2245,9 @@ "passwordProtected": { "message": "密碼保護" }, + "copyLink": { + "message": "Copy link" + }, "copySendLink": { "message": "複製 Send 連結", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2103,6 +2379,24 @@ "message": "Send 已建立", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, + "createdSendSuccessfully": { + "message": "Send created successfully!", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendAvailability": { + "message": "The Send will be available to anyone with the link for the next $DAYS$ days.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.", + "placeholders": { + "days": { + "content": "$1", + "example": "5" + } + } + }, + "sendLinkCopied": { + "message": "Send link copied", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "editedSend": { "message": "Send 已儲存", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." @@ -2164,6 +2458,9 @@ "emailVerificationRequired": { "message": "需要驗證電子郵件" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "您必須驗證您的電子郵件才能使用此功能。您可以在網頁密碼庫裡驗證您的電子郵件。" }, @@ -2179,6 +2476,9 @@ "updateWeakMasterPasswordWarning": { "message": "您的主密碼不符合一個或多個組織原則要求。您必須立即更新您的主密碼才能存取密碼庫。進行此動作將登出您目前的工作階段,需要您重新登入。其他裝置上的工作階段可能繼續長達一小時。" }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "resetPasswordPolicyAutoEnroll": { "message": "自動註冊" }, @@ -2629,14 +2929,23 @@ "autofillSettings": { "message": "自動填入設定" }, + "autofillKeyboardShortcutSectionTitle": { + "message": "Autofill shortcut" + }, + "autofillKeyboardShortcutUpdateLabel": { + "message": "Change shortcut" + }, + "autofillKeyboardManagerShortcutsLabel": { + "message": "Manage shortcuts" + }, "autofillShortcut": { "message": "自動填入鍵盤快速鍵" }, - "autofillShortcutNotSet": { - "message": "自動填入快速鍵尚未設定。請在瀏覽器的設定中變更。" + "autofillLoginShortcutNotSet": { + "message": "The autofill login shortcut is not set. Change this in the browser's settings." }, - "autofillShortcutText": { - "message": "自動填入快速鍵:$COMMAND$。請在瀏覽器的設定中變更。", + "autofillLoginShortcutText": { + "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", "placeholders": { "command": { "content": "$1", @@ -2738,6 +3047,14 @@ "deviceTrusted": { "message": "裝置已信任" }, + "sendsNoItemsTitle": { + "message": "No active Sends", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, + "sendsNoItemsMessage": { + "message": "Use Send to securely share encrypted information with anyone.", + "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." + }, "inputRequired": { "message": "必須輸入內容。" }, @@ -2811,6 +3128,18 @@ } } }, + "singleFieldNeedsAttention": { + "message": "1 field needs your attention." + }, + "multipleFieldsNeedAttention": { + "message": "$COUNT$ fields need your attention.", + "placeholders": { + "count": { + "content": "$1", + "example": "2" + } + } + }, "selectPlaceholder": { "message": "-- 選擇 --" }, @@ -2879,11 +3208,11 @@ }, "passwordRepromptDisabledAutofillOnPageLoad": { "message": "使用主密碼重新提示的項目無法在頁面載入時自動填寫。已關閉頁面載入時的自動填入。", - "description": "Toast message for describing that master password re-prompt cannot be auto-filled on page load." + "description": "Toast message for describing that master password re-prompt cannot be autofilled on page load." }, "autofillOnPageLoadSetToDefault": { "message": "將頁面載入時的自動填入設定為使用預設設定。", - "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." + "description": "Toast message for informing the user that autofill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { "message": "關閉主密碼重新提示以編輯此欄位", @@ -2911,10 +3240,18 @@ "message": "解鎖您的帳戶以查看符合的登入資訊", "description": "Text to display in overlay when the account is locked." }, + "unlockYourAccountToViewAutofillSuggestions": { + "message": "Unlock your account to view autofill suggestions", + "description": "Text to display in overlay when the account is locked." + }, "unlockAccount": { "message": "解鎖帳號", "description": "Button text to display in overlay when the account is locked." }, + "unlockAccountAria": { + "message": "Unlock your account, opens in a new window", + "description": "Screen reader text (aria-label) for unlock account button in overlay" + }, "fillCredentialsFor": { "message": "填入登入資訊給", "description": "Screen reader text for when overlay item is in focused" @@ -2935,6 +3272,30 @@ "message": "新增密碼庫項目", "description": "Screen reader text (aria-label) for new item button in overlay" }, + "newLogin": { + "message": "New 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", + "description": "Screen reader text (aria-label) for new login button within inline menu" + }, + "newCard": { + "message": "New card", + "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", + "description": "Screen reader text (aria-label) for new card button within inline menu" + }, + "newIdentity": { + "message": "New identity", + "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", + "description": "Screen reader text (aria-label) for new identity button within inline menu" + }, "bitwardenOverlayMenuAvailable": { "message": "Bitwarden 自動填入選單可用。按下箭頭鍵來選擇。", "description": "Screen reader text for announcing when the overlay opens on the page" @@ -3021,6 +3382,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "啟動 Duo 並依照步驟完成登入。" }, @@ -3048,8 +3412,8 @@ "invalidFilePassword": { "message": "檔案密碼無效,請使用您當初匯出檔案時輸入的密碼。" }, - "importDestination": { - "message": "匯入目的地" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "瞭解更多匯入選項" @@ -3122,8 +3486,8 @@ "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { "message": "發起站點需要驗證。對於沒有主密碼的帳戶,此功能尚未實現。" }, - "logInWithPasskey": { - "message": "使用密碼金鑰登入?" + "logInWithPasskeyQuestion": { + "message": "Log in with passkey?" }, "passkeyAlreadyExists": { "message": "用於這個應用程式的密碼金鑰已經存在。" @@ -3134,6 +3498,9 @@ "noMatchingPasskeyLogin": { "message": "您沒有適用於此網站的登入項目。" }, + "noMatchingLoginsForSite": { + "message": "No matching logins for this site" + }, "confirm": { "message": "確認" }, @@ -3143,8 +3510,11 @@ "savePasskeyNewLogin": { "message": "儲存密碼金鑰為新登入項目" }, - "choosePasskey": { - "message": "選擇一個用於儲存此密碼金鑰的登入項目" + "chooseCipherForPasskeySave": { + "message": "Choose a login to save this passkey to" + }, + "chooseCipherForPasskeyAuth": { + "message": "Choose a passkey to log in with" }, "passkeyItem": { "message": "密碼金鑰項目" @@ -3281,7 +3651,7 @@ "placeholders": { "domain": { "content": "$1", - "example": "google.com" + "example": "duckduckgo.com" } } }, @@ -3289,12 +3659,36 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "confirmContinueToBrowserSettingsTitle": { + "message": "Continue to browser settings?", + "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" + }, + "confirmContinueToHelpCenter": { + "message": "Continue to Help Center?", + "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.", + "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.", + "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.", + "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.", + "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" + }, "overrideDefaultBrowserAutofillTitle": { "message": "Make Bitwarden your default password manager?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { @@ -3317,10 +3711,18 @@ "message": "Credentials saved successfully!", "description": "Notification message for when saving credentials has succeeded." }, + "passwordSaved": { + "message": "Password saved!", + "description": "Notification message for when saving credentials has succeeded." + }, "updateCipherAttemptSuccess": { "message": "Credentials updated successfully!", "description": "Notification message for when updating credentials has succeeded." }, + "passwordUpdated": { + "message": "Password updated!", + "description": "Notification message for when updating credentials has succeeded." + }, "saveCipherAttemptFailed": { "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." @@ -3334,25 +3736,11 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBannerNotice": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." - }, - "unassignedItemsBannerSelfHostNotice": { - "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." - }, - "unassignedItemsBannerCTAPartOne": { - "message": "Assign these items to a collection from the", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, - "unassignedItemsBannerCTAPartTwo": { - "message": "to make them visible.", - "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." - }, "autofillSuggestions": { - "message": "Auto-fill suggestions" + "message": "Autofill suggestions" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to auto-fill" + "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { "message": "Your vault is empty" @@ -3414,8 +3802,8 @@ } }, "autofillTitle": { - "message": "Auto-fill - $ITEMNAME$", - "description": "Title for a button that auto-fills a login item.", + "message": "Autofill - $ITEMNAME$", + "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { "content": "$1", @@ -3426,8 +3814,8 @@ "noValuesToCopy": { "message": "No values to copy" }, - "assignCollections": { - "message": "Assign collections" + "assignToCollections": { + "message": "Assign to collections" }, "copyEmail": { "message": "Copy email" @@ -3520,6 +3908,24 @@ "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou": { + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, "upload": { "message": "Upload" }, @@ -3558,5 +3964,379 @@ }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardNumberEndsWith": { + "message": "card number ends with", + "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." + }, + "loginCredentials": { + "message": "Login credentials" + }, + "authenticatorKey": { + "message": "Authenticator key" + }, + "autofillOptions": { + "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" + }, + "addWebsite": { + "message": "Add website" + }, + "deleteWebsite": { + "message": "Delete website" + }, + "defaultLabel": { + "message": "Default ($VALUE$)", + "description": "A label that indicates the default value for a field with the current default value in parentheses.", + "placeholders": { + "value": { + "content": "$1", + "example": "Base domain" + } + } + }, + "showMatchDetection": { + "message": "Show match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "hideMatchDetection": { + "message": "Hide match detection $WEBSITE$", + "placeholders": { + "website": { + "content": "$1", + "example": "https://example.com" + } + } + }, + "autoFillOnPageLoad": { + "message": "Autofill on page load?" + }, + "cardExpiredTitle": { + "message": "Expired card" + }, + "cardExpiredMessage": { + "message": "If you've renewed it, update the card's information" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } + }, + "enableAnimations": { + "message": "Enable animations" + }, + "addAccount": { + "message": "Add account" + }, + "loading": { + "message": "Loading" + }, + "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" + }, + "bulkCollectionAssignmentDialogDescriptionSingular": { + "message": "Only organization members with access to these collections will be able to see the item." + }, + "bulkCollectionAssignmentDialogDescriptionPlural": { + "message": "Only organization members with access to these collections will be able to see the items." + }, + "bulkCollectionAssignmentWarning": { + "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "placeholders": { + "total_count": { + "content": "$1", + "example": "10" + }, + "readonly_count": { + "content": "$2" + } + } + }, + "addField": { + "message": "Add field" + }, + "add": { + "message": "Add" + }, + "fieldType": { + "message": "Field type" + }, + "fieldLabel": { + "message": "Field label" + }, + "textHelpText": { + "message": "Use text fields for data like security questions" + }, + "hiddenHelpText": { + "message": "Use hidden fields for sensitive data like a password" + }, + "checkBoxHelpText": { + "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + }, + "linkedHelpText": { + "message": "Use a linked field when you are experiencing autofill issues for a specific website." + }, + "linkedLabelHelpText": { + "message": "Enter the the field's html id, name, aria-label, or placeholder." + }, + "editField": { + "message": "Edit field" + }, + "editFieldLabel": { + "message": "Edit $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "deleteCustomField": { + "message": "Delete $LABEL$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "fieldAdded": { + "message": "$LABEL$ added", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderToggleButton": { + "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + } + } + }, + "reorderFieldUp": { + "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "selectCollectionsToAssign": { + "message": "Select collections to assign" + }, + "personalItemTransferWarningSingular": { + "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + }, + "personalItemsTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + } + } + }, + "personalItemWithOrgTransferWarningSingular": { + "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "placeholders": { + "org": { + "content": "$1", + "example": "Organization name" + } + } + }, + "personalItemsWithOrgTransferWarningPlural": { + "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "placeholders": { + "personal_items_count": { + "content": "$1", + "example": "2" + }, + "org": { + "content": "$2", + "example": "Organization name" + } + } + }, + "successfullyAssignedCollections": { + "message": "Successfully assigned collections" + }, + "nothingSelected": { + "message": "You have not selected anything." + }, + "movedItemsToOrg": { + "message": "Selected items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemsMovedToOrg": { + "message": "Items moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "itemMovedToOrg": { + "message": "Item moved to $ORGNAME$", + "placeholders": { + "orgname": { + "content": "$1", + "example": "Company Name" + } + } + }, + "reorderFieldDown": { + "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "placeholders": { + "label": { + "content": "$1", + "example": "Custom field" + }, + "index": { + "content": "$2", + "example": "1" + }, + "length": { + "content": "$3", + "example": "3" + } + } + }, + "itemLocation": { + "message": "Item Location" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "bitwardenNewLook": { + "message": "Bitwarden has a new look!" + }, + "bitwardenNewLookDesc": { + "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" + }, + "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" + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." + }, + "showCharacterCount": { + "message": "Show character count" + }, + "hideCharacterCount": { + "message": "Hide character count" + }, + "itemsInTrash": { + "message": "Items in trash" + }, + "noItemsInTrash": { + "message": "No items in trash" + }, + "noItemsInTrashDesc": { + "message": "Items you delete will appear here and be permanently deleted after 30 days" + }, + "trashWarning": { + "message": "Items that have been in trash more than 30 days will automatically be deleted" + }, + "restore": { + "message": "Restore" + }, + "deleteForever": { + "message": "Delete forever" + }, + "noEditPermissions": { + "message": "You don't have permission to edit this item" } } diff --git a/apps/browser/src/auth/popup/account-switching/account-switcher.component.html b/apps/browser/src/auth/popup/account-switching/account-switcher.component.html index 806dae084dd..f0723d75ff8 100644 --- a/apps/browser/src/auth/popup/account-switching/account-switcher.component.html +++ b/apps/browser/src/auth/popup/account-switching/account-switcher.component.html @@ -1,78 +1,182 @@ - -
- -
-
{{ "switchAccounts" | i18n }}
-
+ + + + + + + + -
- -
-
-
-
-
    + + -
  • - -
  • -
    - {{ "availableAccounts" | i18n }} +
    +
    -
  • - -
  • + + + +

    {{ "availableAccounts" | i18n }}

    +
    + +
    + +
    +
    -
- -

- {{ "accountLimitReached" | i18n }} -

-
+ + +

+ {{ "accountLimitReached" | i18n }} +

+ +
-
{{ "options" | i18n }}
-
- + + + + + + + + +
+ + + + + +
+ +
+
{{ "switchAccounts" | i18n }}
+
+ +
+ +
+
+
+
+
    + +
  • + +
  • + +
    + {{ "availableAccounts" | i18n }} +
    +
  • + +
  • +
    +
    +
+ +

- - {{ "lockNow" | i18n }} - - - + {{ "accountLimitReached" | i18n }} +

+
+ +
+
{{ "options" | i18n }}
+
+ + + +
-
-
+ +
diff --git a/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts b/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts index fbb9075156f..e5c09db6428 100644 --- a/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts +++ b/apps/browser/src/auth/popup/account-switching/account-switcher.component.ts @@ -1,22 +1,55 @@ -import { Location } from "@angular/common"; +import { CommonModule, Location } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { Router } from "@angular/router"; -import { Subject, firstValueFrom, map, of, switchMap, takeUntil } from "rxjs"; +import { Subject, firstValueFrom, map, of, startWith, switchMap, takeUntil } from "rxjs"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { UserId } from "@bitwarden/common/types/guid"; -import { DialogService } from "@bitwarden/components"; +import { + AvatarModule, + ButtonModule, + DialogService, + ItemModule, + SectionComponent, + SectionHeaderComponent, +} from "@bitwarden/components"; +import { enableAccountSwitching } from "../../../platform/flags"; +import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; +import { HeaderComponent } from "../../../platform/popup/header.component"; +import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component"; + +import { AccountComponent } from "./account.component"; +import { CurrentAccountComponent } from "./current-account.component"; import { AccountSwitcherService } from "./services/account-switcher.service"; @Component({ + standalone: true, templateUrl: "account-switcher.component.html", + imports: [ + CommonModule, + JslibModule, + ButtonModule, + ItemModule, + AvatarModule, + PopupPageComponent, + PopupHeaderComponent, + HeaderComponent, + PopOutComponent, + CurrentAccountComponent, + AccountComponent, + SectionComponent, + SectionHeaderComponent, + ], }) export class AccountSwitcherComponent implements OnInit, OnDestroy { readonly lockedStatus = AuthenticationStatus.Locked; @@ -24,17 +57,19 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { loading = false; activeUserCanLock = false; + extensionRefreshFlag = false; + enableAccountSwitching = true; constructor( private accountSwitcherService: AccountSwitcherService, private accountService: AccountService, private vaultTimeoutService: VaultTimeoutService, - private messagingService: MessagingService, private dialogService: DialogService, private location: Location, private router: Router, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private authService: AuthService, + private configService: ConfigService, ) {} get accountLimit() { @@ -54,7 +89,28 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { ), ); + readonly showLockAll$ = this.availableAccounts$.pipe( + startWith([]), + map((accounts) => accounts.filter((a) => !a.isActive)), + switchMap((accounts) => { + // If account switching is disabled, don't show the lock all button + // as only one account should be shown. + if (!enableAccountSwitching()) { + return of(false); + } + + // When there are an inactive accounts provide the option to lock all accounts + // Note: "Add account" is counted as an inactive account, so check for more than one account + return of(accounts.length > 1); + }), + ); + async ngOnInit() { + this.enableAccountSwitching = enableAccountSwitching(); + this.extensionRefreshFlag = await this.configService.getFeatureFlag( + FeatureFlag.ExtensionRefresh, + ); + const availableVaultTimeoutActions = await firstValueFrom( this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(), ); diff --git a/apps/browser/src/auth/popup/account-switching/account.component.html b/apps/browser/src/auth/popup/account-switching/account.component.html index 301a127a7d9..d062c67a2e3 100644 --- a/apps/browser/src/auth/popup/account-switching/account.component.html +++ b/apps/browser/src/auth/popup/account-switching/account.component.html @@ -1,54 +1,109 @@ - + + + + + {{ "activeAccount" | i18n }}: + + + {{ "switchToAccount" | i18n }} + +
+ {{ account.email }} +
+ + +
+ {{ "hostedAt" | i18n }} + {{ account.server }} +
+ +
+ ( + {{ + status.text + }} + ) +
+
+ + + +
+ + + + +
+ + + + + + diff --git a/apps/browser/src/auth/popup/account-switching/account.component.ts b/apps/browser/src/auth/popup/account-switching/account.component.ts index 3f152d61b92..d54d6fe0e29 100644 --- a/apps/browser/src/auth/popup/account-switching/account.component.ts +++ b/apps/browser/src/auth/popup/account-switching/account.component.ts @@ -5,7 +5,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { AvatarModule } from "@bitwarden/components"; +import { AvatarModule, ItemModule } from "@bitwarden/components"; import { AccountSwitcherService, AvailableAccount } from "./services/account-switcher.service"; @@ -13,10 +13,11 @@ import { AccountSwitcherService, AvailableAccount } from "./services/account-swi standalone: true, selector: "auth-account", templateUrl: "account.component.html", - imports: [CommonModule, JslibModule, AvatarModule], + imports: [CommonModule, JslibModule, AvatarModule, ItemModule], }) export class AccountComponent { @Input() account: AvailableAccount; + @Input() extensionRefreshFlag: boolean = false; @Output() loading = new EventEmitter(); constructor( diff --git a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts index d60b0dfaebc..7cc9f8a92f2 100644 --- a/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts +++ b/apps/browser/src/auth/popup/account-switching/services/account-switcher.service.ts @@ -80,7 +80,7 @@ export class AccountSwitcherService { if (!hasMaxAccounts) { options.push({ - name: "Add account", + name: "addAccount", id: this.SPECIAL_ADD_ACCOUNT_ID, isActive: false, }); diff --git a/apps/browser/src/auth/popup/environment.component.ts b/apps/browser/src/auth/popup/environment.component.ts index ed348e563b6..b84f03b5fd7 100644 --- a/apps/browser/src/auth/popup/environment.component.ts +++ b/apps/browser/src/auth/popup/environment.component.ts @@ -5,6 +5,7 @@ import { EnvironmentComponent as BaseEnvironmentComponent } from "@bitwarden/ang import { ModalService } from "@bitwarden/angular/services/modal.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; @@ -21,8 +22,9 @@ export class EnvironmentComponent extends BaseEnvironmentComponent implements On i18nService: I18nService, private router: Router, modalService: ModalService, + toastService: ToastService, ) { - super(platformUtilsService, environmentService, i18nService, modalService); + super(platformUtilsService, environmentService, i18nService, modalService, toastService); this.showCustom = true; } diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service.ts new file mode 100644 index 00000000000..1b844d4b2c7 --- /dev/null +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service.ts @@ -0,0 +1,23 @@ +import { Observable, Subject } from "rxjs"; + +import { + AnonLayoutWrapperDataService, + DefaultAnonLayoutWrapperDataService, +} from "@bitwarden/auth/angular"; + +import { ExtensionAnonLayoutWrapperData } from "./extension-anon-layout-wrapper.component"; + +export class ExtensionAnonLayoutWrapperDataService + extends DefaultAnonLayoutWrapperDataService + implements AnonLayoutWrapperDataService +{ + protected override anonLayoutWrapperDataSubject = new Subject(); + + override setAnonLayoutWrapperData(data: ExtensionAnonLayoutWrapperData): void { + this.anonLayoutWrapperDataSubject.next(data); + } + + override anonLayoutWrapperData$(): Observable { + return this.anonLayoutWrapperDataSubject.asObservable(); + } +} 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 new file mode 100644 index 00000000000..d5273fd9fb2 --- /dev/null +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + 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 new file mode 100644 index 00000000000..350b4a8a84d --- /dev/null +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component.ts @@ -0,0 +1,186 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ActivatedRoute, Data, NavigationEnd, Router, RouterModule } from "@angular/router"; +import { Subject, filter, switchMap, takeUntil, tap } from "rxjs"; + +import { + AnonLayoutComponent, + AnonLayoutWrapperData, + AnonLayoutWrapperDataService, +} from "@bitwarden/auth/angular"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Icon, IconModule } from "@bitwarden/components"; + +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 { CurrentAccountComponent } from "../account-switching/current-account.component"; + +import { ExtensionBitwardenLogo } from "./extension-bitwarden-logo.icon"; + +export interface ExtensionAnonLayoutWrapperData extends AnonLayoutWrapperData { + showAcctSwitcher?: boolean; + showBackButton?: boolean; + showLogo?: boolean; +} + +@Component({ + standalone: true, + templateUrl: "extension-anon-layout-wrapper.component.html", + imports: [ + AnonLayoutComponent, + CommonModule, + CurrentAccountComponent, + IconModule, + PopOutComponent, + PopupPageComponent, + PopupHeaderComponent, + RouterModule, + ], +}) +export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy { + private destroy$ = new Subject(); + + protected showAcctSwitcher: boolean; + protected showBackButton: boolean; + protected showLogo: boolean = true; + + protected pageTitle: string; + protected pageSubtitle: string; + protected pageIcon: Icon; + protected showReadonlyHostname: boolean; + protected maxWidth: "md" | "3xl"; + + protected theme: string; + protected logo = ExtensionBitwardenLogo; + + constructor( + private router: Router, + private route: ActivatedRoute, + private i18nService: I18nService, + private extensionAnonLayoutWrapperDataService: AnonLayoutWrapperDataService, + ) {} + + async ngOnInit(): Promise { + // Set the initial page data on load + this.setAnonLayoutWrapperDataFromRouteData(this.route.snapshot.firstChild?.data); + + // Listen for page changes and update the page data appropriately + this.listenForPageDataChanges(); + this.listenForServiceDataChanges(); + } + + private listenForPageDataChanges() { + this.router.events + .pipe( + filter((event) => event instanceof NavigationEnd), + // reset page data on page changes + tap(() => this.resetPageData()), + switchMap(() => this.route.firstChild?.data || null), + takeUntil(this.destroy$), + ) + .subscribe((firstChildRouteData: Data | null) => { + this.setAnonLayoutWrapperDataFromRouteData(firstChildRouteData); + }); + } + + private setAnonLayoutWrapperDataFromRouteData(firstChildRouteData: Data | null) { + if (!firstChildRouteData) { + return; + } + + if (firstChildRouteData["pageTitle"] !== undefined) { + this.pageTitle = this.i18nService.t(firstChildRouteData["pageTitle"]); + } + + if (firstChildRouteData["pageSubtitle"] !== undefined) { + this.pageSubtitle = this.i18nService.t(firstChildRouteData["pageSubtitle"]); + } + + if (firstChildRouteData["pageIcon"] !== undefined) { + this.pageIcon = firstChildRouteData["pageIcon"]; + } + + this.showReadonlyHostname = Boolean(firstChildRouteData["showReadonlyHostname"]); + this.maxWidth = firstChildRouteData["maxWidth"]; + + if (firstChildRouteData["showAcctSwitcher"] !== undefined) { + this.showAcctSwitcher = Boolean(firstChildRouteData["showAcctSwitcher"]); + } + + if (firstChildRouteData["showBackButton"] !== undefined) { + this.showBackButton = Boolean(firstChildRouteData["showBackButton"]); + } + + if (firstChildRouteData["showLogo"] !== undefined) { + this.showLogo = Boolean(firstChildRouteData["showLogo"]); + } + } + + private listenForServiceDataChanges() { + this.extensionAnonLayoutWrapperDataService + .anonLayoutWrapperData$() + .pipe(takeUntil(this.destroy$)) + .subscribe((data: ExtensionAnonLayoutWrapperData) => { + this.setAnonLayoutWrapperData(data); + }); + } + + private setAnonLayoutWrapperData(data: ExtensionAnonLayoutWrapperData) { + if (!data) { + return; + } + + if (data.pageTitle) { + this.pageTitle = this.i18nService.t(data.pageTitle); + } + + if (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) { + this.pageIcon = data.pageIcon; + } + + if (data.showReadonlyHostname != null) { + this.showReadonlyHostname = data.showReadonlyHostname; + } + + if (data.showAcctSwitcher != null) { + this.showAcctSwitcher = data.showAcctSwitcher; + } + + if (data.showBackButton != null) { + this.showBackButton = data.showBackButton; + } + + if (data.showLogo != null) { + this.showLogo = data.showLogo; + } + } + + private resetPageData() { + this.pageTitle = null; + this.pageSubtitle = null; + this.pageIcon = null; + this.showReadonlyHostname = null; + this.showAcctSwitcher = null; + this.showBackButton = null; + this.showLogo = null; + this.maxWidth = null; + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } +} 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 new file mode 100644 index 00000000000..beb07f3523a --- /dev/null +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts @@ -0,0 +1,294 @@ +import { importProvidersFrom, Component } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; +import { + Meta, + StoryObj, + applicationConfig, + componentWrapperDecorator, + moduleMetadata, +} from "@storybook/angular"; +import { of } from "rxjs"; + +import { AnonLayoutWrapperDataService, LockIcon } from "@bitwarden/auth/angular"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { ClientType } from "@bitwarden/common/enums"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { + EnvironmentService, + Environment, +} from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { 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 { + ExtensionAnonLayoutWrapperComponent, + ExtensionAnonLayoutWrapperData, +} from "./extension-anon-layout-wrapper.component"; + +export default { + title: "Auth/Extension Anon Layout Wrapper", + component: ExtensionAnonLayoutWrapperComponent, +} as Meta; + +const decorators = (options: { + components: any[]; + routes: Routes; + applicationVersion?: string; + clientType?: ClientType; + hostName?: string; +}) => { + return [ + componentWrapperDecorator( + /** + * Applying a CSS transform makes a `position: fixed` element act like it is `position: relative` + * https://github.com/storybookjs/storybook/issues/8011#issue-490251969 + */ + (story) => { + return /* HTML */ `
${story}
`; + }, + ({ globals }) => { + /** + * avoid a bug with the way that we render the same component twice in the same iframe and how + * that interacts with the router-outlet + */ + const themeOverride = globals["theme"] === "both" ? "light" : globals["theme"]; + return { theme: themeOverride }; + }, + ), + moduleMetadata({ + declarations: options.components, + imports: [RouterModule, ButtonModule], + providers: [ + { + provide: AnonLayoutWrapperDataService, + useClass: ExtensionAnonLayoutWrapperDataService, + }, + { + provide: AccountService, + useValue: { + activeAccount$: of({ + id: "test-user-id" as UserId, + name: "Test User 1", + email: "test@email.com", + emailVerified: true, + }), + }, + }, + { + provide: AuthService, + useValue: { + activeAccountStatus$: of(AuthenticationStatus.Unlocked), + }, + }, + { + provide: AvatarService, + useValue: { + avatarColor$: of("#ab134a"), + } as Partial, + }, + { + provide: ConfigService, + useValue: { + getFeatureFlag: () => true, + }, + }, + { + provide: EnvironmentService, + useValue: { + environment$: of({ + getHostname: () => options.hostName || "storybook.bitwarden.com", + } as Partial), + } as Partial, + }, + { + provide: PlatformUtilsService, + useValue: { + getApplicationVersion: () => + Promise.resolve(options.applicationVersion || "FAKE_APP_VERSION"), + getClientType: () => options.clientType || ClientType.Web, + } as Partial, + }, + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + setAStrongPassword: "Set a strong password", + finishCreatingYourAccountBySettingAPassword: + "Finish creating your account by setting a password", + enterpriseSingleSignOn: "Enterprise single sign-on", + checkYourEmail: "Check your email", + loading: "Loading", + popOutNewWindow: "Pop out to a new window", + switchAccounts: "Switch accounts", + back: "Back", + activeAccount: "Active account", + }); + }, + }, + ], + }), + applicationConfig({ + providers: [ + importProvidersFrom(RouterModule.forRoot(options.routes)), + { + provide: PopupRouterCacheService, + useValue: { + back() {}, + } as Partial, + }, + ], + }), + ]; +}; + +type Story = StoryObj; + +// Default Example + +@Component({ + selector: "bit-default-primary-outlet-example-component", + template: "

Primary Outlet Example:
your primary component goes here

", +}) +class DefaultPrimaryOutletExampleComponent {} + +@Component({ + selector: "bit-default-secondary-outlet-example-component", + template: "

Secondary Outlet Example:
your secondary component goes here

", +}) +class DefaultSecondaryOutletExampleComponent {} + +@Component({ + selector: "bit-default-env-selector-outlet-example-component", + template: "

Env Selector Outlet Example:
your env selector component goes here

", +}) +class DefaultEnvSelectorOutletExampleComponent {} + +export const DefaultContentExample: Story = { + render: (args) => ({ + props: args, + template: "", + }), + decorators: decorators({ + components: [ + DefaultPrimaryOutletExampleComponent, + DefaultSecondaryOutletExampleComponent, + DefaultEnvSelectorOutletExampleComponent, + ], + routes: [ + { + path: "**", + redirectTo: "default-example", + pathMatch: "full", + }, + { + path: "", + component: ExtensionAnonLayoutWrapperComponent, + children: [ + { + path: "default-example", + data: {}, + children: [ + { + path: "", + component: DefaultPrimaryOutletExampleComponent, + }, + { + path: "", + component: DefaultSecondaryOutletExampleComponent, + outlet: "secondary", + }, + { + path: "", + component: DefaultEnvSelectorOutletExampleComponent, + outlet: "environment-selector", + }, + ], + }, + ], + }, + ], + }), +}; + +// Dynamic Content Example +const initialData: ExtensionAnonLayoutWrapperData = { + pageTitle: "setAStrongPassword", + pageSubtitle: "finishCreatingYourAccountBySettingAPassword", + pageIcon: LockIcon, + showAcctSwitcher: true, + showBackButton: true, + showLogo: true, +}; + +const changedData: ExtensionAnonLayoutWrapperData = { + pageTitle: "enterpriseSingleSignOn", + pageSubtitle: "checkYourEmail", + pageIcon: RegistrationCheckEmailIcon, + showAcctSwitcher: false, + showBackButton: false, + showLogo: false, +}; + +@Component({ + selector: "bit-dynamic-content-example-component", + template: ` + + `, +}) +export class DynamicContentExampleComponent { + initialData = true; + + constructor(private extensionAnonLayoutWrapperDataService: AnonLayoutWrapperDataService) {} + + toggleData() { + if (this.initialData) { + this.extensionAnonLayoutWrapperDataService.setAnonLayoutWrapperData(changedData); + } else { + this.extensionAnonLayoutWrapperDataService.setAnonLayoutWrapperData(initialData); + } + + this.initialData = !this.initialData; + } +} + +export const DynamicContentExample: Story = { + render: (args) => ({ + props: args, + template: "", + }), + decorators: decorators({ + components: [DynamicContentExampleComponent], + routes: [ + { + path: "**", + redirectTo: "dynamic-content-example", + pathMatch: "full", + }, + { + path: "", + component: ExtensionAnonLayoutWrapperComponent, + children: [ + { + path: "dynamic-content-example", + data: initialData, + children: [ + { + path: "", + component: DynamicContentExampleComponent, + }, + ], + }, + ], + }, + ], + }), +}; diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-bitwarden-logo.icon.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-bitwarden-logo.icon.ts new file mode 100644 index 00000000000..51d748e1fbb --- /dev/null +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-bitwarden-logo.icon.ts @@ -0,0 +1,18 @@ +import { svgIcon } from "@bitwarden/components"; + +export const ExtensionBitwardenLogo = svgIcon` + + + +`; diff --git a/apps/browser/src/auth/popup/hint.component.ts b/apps/browser/src/auth/popup/hint.component.ts index 214a43efb71..bc1f68f4c43 100644 --- a/apps/browser/src/auth/popup/hint.component.ts +++ b/apps/browser/src/auth/popup/hint.component.ts @@ -7,6 +7,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.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 { ToastService } from "@bitwarden/components"; @Component({ selector: "app-hint", @@ -21,8 +22,17 @@ export class HintComponent extends BaseHintComponent { logService: LogService, private route: ActivatedRoute, loginEmailService: LoginEmailServiceAbstraction, + toastService: ToastService, ) { - super(router, i18nService, apiService, platformUtilsService, logService, loginEmailService); + super( + router, + i18nService, + apiService, + platformUtilsService, + logService, + loginEmailService, + toastService, + ); super.onSuccessfulSubmit = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/browser/src/auth/popup/home.component.html b/apps/browser/src/auth/popup/home.component.html index 35371948de9..ed395797961 100644 --- a/apps/browser/src/auth/popup/home.component.html +++ b/apps/browser/src/auth/popup/home.component.html @@ -30,7 +30,7 @@ diff --git a/apps/browser/src/auth/popup/home.component.ts b/apps/browser/src/auth/popup/home.component.ts index e647dfd05b9..cd9dfc3702b 100644 --- a/apps/browser/src/auth/popup/home.component.ts +++ b/apps/browser/src/auth/popup/home.component.ts @@ -1,15 +1,13 @@ import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { Router } from "@angular/router"; -import { Subject, firstValueFrom, takeUntil } from "rxjs"; +import { Subject, firstValueFrom, switchMap, takeUntil } from "rxjs"; import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { LoginEmailServiceAbstraction, RegisterRouteService } from "@bitwarden/auth/common"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; import { AccountSwitcherService } from "./account-switching/services/account-switcher.service"; @@ -29,30 +27,21 @@ export class HomeComponent implements OnInit, OnDestroy { }); // TODO: remove when email verification flag is removed - registerRoute = "/register"; + registerRoute$ = this.registerRouteService.registerRoute$(); constructor( protected platformUtilsService: PlatformUtilsService, private formBuilder: FormBuilder, private router: Router, private i18nService: I18nService, - private environmentService: EnvironmentService, private loginEmailService: LoginEmailServiceAbstraction, private accountSwitcherService: AccountSwitcherService, - private configService: ConfigService, + private registerRouteService: RegisterRouteService, + private toastService: ToastService, ) {} async ngOnInit(): Promise { - // TODO: remove when email verification flag is removed - const emailVerification = await this.configService.getFeatureFlag( - FeatureFlag.EmailVerification, - ); - - if (emailVerification) { - this.registerRoute = "/signup"; - } - - const email = this.loginEmailService.getEmail(); + const email = await firstValueFrom(this.loginEmailService.loginEmail$); const rememberEmail = this.loginEmailService.getRememberEmail(); if (email != null) { @@ -66,13 +55,14 @@ export class HomeComponent implements OnInit, OnDestroy { } this.environmentSelector.onOpenSelfHostedSettings - .pipe(takeUntil(this.destroyed$)) - .subscribe(() => { - this.setLoginEmailValues(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["environment"]); - }); + .pipe( + switchMap(async () => { + await this.setLoginEmailValues(); + await this.router.navigate(["environment"]); + }), + takeUntil(this.destroyed$), + ) + .subscribe(); } ngOnDestroy(): void { @@ -88,20 +78,22 @@ export class HomeComponent implements OnInit, OnDestroy { this.formGroup.markAllAsTouched(); if (this.formGroup.invalid) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccured"), - this.i18nService.t("invalidEmail"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccured"), + message: this.i18nService.t("invalidEmail"), + }); return; } - this.setLoginEmailValues(); + await this.setLoginEmailValues(); await this.router.navigate(["login"], { queryParams: { email: this.formGroup.value.email } }); } - setLoginEmailValues() { - this.loginEmailService.setEmail(this.formGroup.value.email); + async setLoginEmailValues() { + // Note: Browser saves email settings here instead of the login component this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail); + await this.loginEmailService.setLoginEmail(this.formGroup.value.email); + await this.loginEmailService.saveEmailSettings(); } } diff --git a/apps/browser/src/auth/popup/lock.component.html b/apps/browser/src/auth/popup/lock.component.html index 5ea839470be..fb1b09de49c 100644 --- a/apps/browser/src/auth/popup/lock.component.html +++ b/apps/browser/src/auth/popup/lock.component.html @@ -89,12 +89,12 @@

- {{ biometricError }} + {{ biometricError }}

{{ "awaitDesktop" | i18n }}

- + diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index 5047889b8ec..f5413e4bea4 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -1,4 +1,4 @@ -import { Component, NgZone } from "@angular/core"; +import { Component, NgZone, OnInit } from "@angular/core"; import { Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; @@ -24,9 +24,10 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { BiometricErrors, BiometricErrorTypes } from "../../models/biometricErrors"; import { BrowserRouterService } from "../../platform/popup/services/browser-router.service"; @@ -36,7 +37,7 @@ import { fido2PopoutSessionData$ } from "../../vault/popup/utils/fido2-popout-se selector: "app-lock", templateUrl: "lock.component.html", }) -export class LockComponent extends BaseLockComponent { +export class LockComponent extends BaseLockComponent implements OnInit { private isInitialLockScreen: boolean; biometricError: string; @@ -67,9 +68,11 @@ export class LockComponent extends BaseLockComponent { pinService: PinServiceAbstraction, private routerService: BrowserRouterService, biometricStateService: BiometricStateService, + biometricsService: BiometricsService, accountService: AccountService, kdfConfigService: KdfConfigService, syncService: SyncService, + toastService: ToastService, ) { super( masterPasswordService, @@ -93,10 +96,12 @@ export class LockComponent extends BaseLockComponent { userVerificationService, pinService, biometricStateService, + biometricsService, accountService, authService, kdfConfigService, syncService, + toastService, ); this.successRoute = "/tabs/current"; this.isInitialLockScreen = (window as any).previousPopupUrl == null; @@ -129,22 +134,35 @@ export class LockComponent extends BaseLockComponent { this.isInitialLockScreen && (await this.authService.getAuthStatus()) === AuthenticationStatus.Locked ) { - await this.unlockBiometric(); + await this.unlockBiometric(true); } }, 100); } - override async unlockBiometric(): Promise { + override async unlockBiometric(automaticPrompt: boolean = false): Promise { if (!this.biometricLock) { return; } - this.pendingBiometric = true; this.biometricError = null; let success; try { - success = await super.unlockBiometric(); + const available = await super.isBiometricUnlockAvailable(); + if (!available) { + if (!automaticPrompt) { + await this.dialogService.openSimpleDialog({ + type: "warning", + title: { key: "biometricsNotAvailableTitle" }, + content: { key: "biometricsNotAvailableDesc" }, + acceptButtonText: { key: "ok" }, + cancelButtonText: null, + }); + } + } else { + this.pendingBiometric = true; + success = await super.unlockBiometric(); + } } catch (e) { const error = BiometricErrors[e?.message as BiometricErrorTypes]; diff --git a/apps/browser/src/auth/popup/login-via-auth-request.component.ts b/apps/browser/src/auth/popup/login-via-auth-request.component.ts index f83062e6c97..53f29badee6 100644 --- a/apps/browser/src/auth/popup/login-via-auth-request.component.ts +++ b/apps/browser/src/auth/popup/login-via-auth-request.component.ts @@ -22,6 +22,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @Component({ @@ -50,6 +51,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { loginStrategyService: LoginStrategyServiceAbstraction, accountService: AccountService, private location: Location, + toastService: ToastService, ) { super( router, @@ -70,6 +72,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { deviceTrustService, authRequestService, loginStrategyService, + toastService, ); super.onSuccessfulLogin = async () => { await syncService.fullSync(true); diff --git a/apps/browser/src/auth/popup/login.component.html b/apps/browser/src/auth/popup/login.component.html index 7a4211a5cc1..9d2b4fccad7 100644 --- a/apps/browser/src/auth/popup/login.component.html +++ b/apps/browser/src/auth/popup/login.component.html @@ -52,7 +52,7 @@ diff --git a/apps/browser/src/auth/popup/login.component.ts b/apps/browser/src/auth/popup/login.component.ts index 760db66c311..ea72fb61f5f 100644 --- a/apps/browser/src/auth/popup/login.component.ts +++ b/apps/browser/src/auth/popup/login.component.ts @@ -1,4 +1,4 @@ -import { Component, NgZone } from "@angular/core"; +import { Component, NgZone, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; @@ -8,12 +8,12 @@ import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstrac import { LoginStrategyServiceAbstraction, LoginEmailServiceAbstraction, + RegisterRouteService, } from "@bitwarden/auth/common"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -22,6 +22,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { flagEnabled } from "../../platform/flags"; @@ -30,7 +31,7 @@ import { flagEnabled } from "../../platform/flags"; selector: "app-login", templateUrl: "login.component.html", }) -export class LoginComponent extends BaseLoginComponent { +export class LoginComponent extends BaseLoginComponent implements OnInit { showPasswordless = false; constructor( devicesApiService: DevicesApiServiceAbstraction, @@ -52,7 +53,8 @@ export class LoginComponent extends BaseLoginComponent { loginEmailService: LoginEmailServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction, - configService: ConfigService, + registerRouteService: RegisterRouteService, + toastService: ToastService, ) { super( devicesApiService, @@ -73,20 +75,20 @@ export class LoginComponent extends BaseLoginComponent { loginEmailService, ssoLoginService, webAuthnLoginService, - configService, + registerRouteService, + toastService, ); super.onSuccessfulLogin = async () => { await syncService.fullSync(true); }; super.successRoute = "/tabs/vault"; this.showPasswordless = flagEnabled("showPasswordless"); + } + async ngOnInit(): Promise { + await super.ngOnInit(); if (this.showPasswordless) { - this.formGroup.controls.email.setValue(this.loginEmailService.getEmail()); - this.formGroup.controls.rememberEmail.setValue(this.loginEmailService.getRememberEmail()); - // 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.validateEmail(); + await this.validateEmail(); } } @@ -99,7 +101,7 @@ export class LoginComponent extends BaseLoginComponent { async launchSsoBrowser() { // Save off email for SSO await this.ssoLoginService.setSsoEmail(this.formGroup.value.email); - await this.loginEmailService.saveEmailSettings(); + // Generate necessary sso params const passwordOptions: any = { type: "password", @@ -142,4 +144,9 @@ export class LoginComponent extends BaseLoginComponent { encodeURIComponent(this.formGroup.controls.email.value), ); } + + async saveEmailSettings() { + // values should be saved on home component + return; + } } diff --git a/apps/browser/src/auth/popup/register.component.ts b/apps/browser/src/auth/popup/register.component.ts index 61e007ac52a..dab1e62f850 100644 --- a/apps/browser/src/auth/popup/register.component.ts +++ b/apps/browser/src/auth/popup/register.component.ts @@ -13,7 +13,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @Component({ @@ -39,6 +39,7 @@ export class RegisterComponent extends BaseRegisterComponent { logService: LogService, auditService: AuditService, dialogService: DialogService, + toastService: ToastService, ) { super( formValidationErrorService, @@ -55,6 +56,7 @@ export class RegisterComponent extends BaseRegisterComponent { logService, auditService, dialogService, + toastService, ); } } diff --git a/apps/browser/src/auth/popup/services/index.ts b/apps/browser/src/auth/popup/services/index.ts deleted file mode 100644 index 63563f61fd9..00000000000 --- a/apps/browser/src/auth/popup/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { UnauthGuardService } from "./unauth-guard.service"; diff --git a/apps/browser/src/auth/popup/services/unauth-guard.service.ts b/apps/browser/src/auth/popup/services/unauth-guard.service.ts deleted file mode 100644 index 0fbb4ac9bae..00000000000 --- a/apps/browser/src/auth/popup/services/unauth-guard.service.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards"; - -export class UnauthGuardService extends BaseUnauthGuardService { - protected homepage = "tabs/current"; -} 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 10c9b2fb98d..25401f06f38 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -1,4 +1,5 @@ -import { ChangeDetectorRef, Component, OnInit } from "@angular/core"; +import { DialogRef } from "@angular/cdk/dialog"; +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { BehaviorSubject, @@ -32,12 +33,13 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { VaultTimeout, VaultTimeoutOption, VaultTimeoutStringType, } from "@bitwarden/common/types/vault-timeout.type"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors"; import { BrowserApi } from "../../../platform/browser/browser-api"; @@ -52,7 +54,7 @@ import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component"; templateUrl: "account-security.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class AccountSecurityComponent implements OnInit { +export class AccountSecurityComponent implements OnInit, OnDestroy { protected readonly VaultTimeoutAction = VaultTimeoutAction; availableVaultTimeoutActions: VaultTimeoutAction[] = []; @@ -93,6 +95,8 @@ export class AccountSecurityComponent implements OnInit { private dialogService: DialogService, private changeDetectorRef: ChangeDetectorRef, private biometricStateService: BiometricStateService, + private toastService: ToastService, + private biometricsService: BiometricsService, ) { this.accountSwitcherEnabled = enableAccountSwitching(); } @@ -164,7 +168,7 @@ export class AccountSecurityComponent implements OnInit { }; this.form.patchValue(initialValues, { emitEvent: false }); - this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); + this.supportsBiometric = await this.biometricsService.supportsBiometric(); this.showChangeMasterPass = await this.userVerificationService.hasMasterPassword(); this.form.controls.vaultTimeout.valueChanges @@ -271,11 +275,11 @@ export class AccountSecurityComponent implements OnInit { // The minTimeoutError does not apply to browser because it supports Immediately // So only check for the policyError if (this.form.controls.vaultTimeout.hasError("policyError")) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("vaultTimeoutTooLarge"), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("vaultTimeoutTooLarge"), + }); return; } @@ -312,11 +316,11 @@ export class AccountSecurityComponent implements OnInit { } if (this.form.controls.vaultTimeout.hasError("policyError")) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("vaultTimeoutTooLarge"), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("vaultTimeoutTooLarge"), + }); return; } @@ -382,49 +386,73 @@ export class AccountSecurityComponent implements OnInit { 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.biometricsService.authenticateBiometric(); + + // prevent duplicate dialog + biometricsResponseReceived = true; + if (awaitDesktopDialogRef) { awaitDesktopDialogRef.close(true); - }), - ]); + } + + this.form.controls.biometric.setValue(result); + if (!result) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorEnableBiometricTitle"), + message: 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/auth/popup/sso.component.ts b/apps/browser/src/auth/popup/sso.component.ts index 33284717ab5..42222c42b97 100644 --- a/apps/browser/src/auth/popup/sso.component.ts +++ b/apps/browser/src/auth/popup/sso.component.ts @@ -22,6 +22,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { BrowserApi } from "../../platform/browser/browser-api"; @@ -51,6 +52,7 @@ export class SsoComponent extends BaseSsoComponent { accountService: AccountService, private authService: AuthService, @Inject(WINDOW) private win: Window, + toastService: ToastService, ) { super( ssoLoginService, @@ -69,6 +71,7 @@ export class SsoComponent extends BaseSsoComponent { configService, masterPasswordService, accountService, + toastService, ); environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => { diff --git a/apps/browser/src/auth/popup/two-factor-auth-duo.component.ts b/apps/browser/src/auth/popup/two-factor-auth-duo.component.ts new file mode 100644 index 00000000000..5bc9f01fc99 --- /dev/null +++ b/apps/browser/src/auth/popup/two-factor-auth-duo.component.ts @@ -0,0 +1,108 @@ +import { DialogModule } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { ReactiveFormsModule, FormsModule } from "@angular/forms"; +import { Subject, Subscription, filter, firstValueFrom, takeUntil } from "rxjs"; + +import { TwoFactorAuthDuoComponent as TwoFactorAuthDuoBaseComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-duo.component"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; + +import { AsyncActionsModule } from "../../../../../libs/components/src/async-actions"; +import { ButtonModule } from "../../../../../libs/components/src/button"; +import { FormFieldModule } from "../../../../../libs/components/src/form-field"; +import { LinkModule } from "../../../../../libs/components/src/link"; +import { I18nPipe } from "../../../../../libs/components/src/shared/i18n.pipe"; +import { TypographyModule } from "../../../../../libs/components/src/typography"; +import { ZonedMessageListenerService } from "../../platform/browser/zoned-message-listener.service"; + +@Component({ + standalone: true, + selector: "app-two-factor-auth-duo", + templateUrl: + "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-duo.component.html", + imports: [ + CommonModule, + JslibModule, + DialogModule, + ButtonModule, + LinkModule, + TypographyModule, + ReactiveFormsModule, + FormFieldModule, + AsyncActionsModule, + FormsModule, + ], + providers: [I18nPipe], +}) +export class TwoFactorAuthDuoComponent + extends TwoFactorAuthDuoBaseComponent + implements OnInit, OnDestroy +{ + private destroy$ = new Subject(); + duoResultSubscription: Subscription; + + constructor( + protected i18nService: I18nService, + protected platformUtilsService: PlatformUtilsService, + private browserMessagingApi: ZonedMessageListenerService, + private environmentService: EnvironmentService, + toastService: ToastService, + ) { + super(i18nService, platformUtilsService, toastService); + } + + async ngOnInit(): Promise { + await super.ngOnInit(); + } + + async ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + protected override setupDuoResultListener() { + if (!this.duoResultSubscription) { + this.duoResultSubscription = this.browserMessagingApi + .messageListener$() + .pipe( + filter((msg: any) => msg.command === "duoResult"), + takeUntil(this.destroy$), + ) + .subscribe((msg: { command: string; code: string; state: string }) => { + this.token.emit(msg.code + "|" + msg.state); + }); + } + } + + override async launchDuoFrameless() { + if (this.duoFramelessUrl === null) { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("duoHealthCheckResultsInNullAuthUrlError"), + }); + return; + } + const duoHandOffMessage = { + title: this.i18nService.t("youSuccessfullyLoggedIn"), + message: this.i18nService.t("youMayCloseThisWindow"), + isCountdown: false, + }; + + // we're using the connector here as a way to set a cookie with translations + // before continuing to the duo frameless url + const env = await firstValueFrom(this.environmentService.environment$); + const launchUrl = + env.getWebVaultUrl() + + "/duo-redirect-connector.html" + + "?duoFramelessUrl=" + + encodeURIComponent(this.duoFramelessUrl) + + "&handOffMessage=" + + encodeURIComponent(JSON.stringify(duoHandOffMessage)); + this.platformUtilsService.launchUri(launchUrl); + } +} diff --git a/apps/browser/src/auth/popup/two-factor-auth-email.component.ts b/apps/browser/src/auth/popup/two-factor-auth-email.component.ts new file mode 100644 index 00000000000..b6211bba05f --- /dev/null +++ b/apps/browser/src/auth/popup/two-factor-auth-email.component.ts @@ -0,0 +1,55 @@ +import { DialogModule } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, OnInit, inject } from "@angular/core"; +import { ReactiveFormsModule, FormsModule } from "@angular/forms"; + +import { TwoFactorAuthEmailComponent as TwoFactorAuthEmailBaseComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-email.component"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; + +import { AsyncActionsModule } from "../../../../../libs/components/src/async-actions"; +import { ButtonModule } from "../../../../../libs/components/src/button"; +import { DialogService } from "../../../../../libs/components/src/dialog"; +import { FormFieldModule } from "../../../../../libs/components/src/form-field"; +import { LinkModule } from "../../../../../libs/components/src/link"; +import { I18nPipe } from "../../../../../libs/components/src/shared/i18n.pipe"; +import { TypographyModule } from "../../../../../libs/components/src/typography"; +import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; + +@Component({ + standalone: true, + selector: "app-two-factor-auth-email", + templateUrl: + "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component.html", + imports: [ + CommonModule, + JslibModule, + DialogModule, + ButtonModule, + LinkModule, + TypographyModule, + ReactiveFormsModule, + FormFieldModule, + AsyncActionsModule, + FormsModule, + ], + providers: [I18nPipe], +}) +export class TwoFactorAuthEmailComponent extends TwoFactorAuthEmailBaseComponent implements OnInit { + private dialogService = inject(DialogService); + + async ngOnInit(): Promise { + if (BrowserPopupUtils.inPopup(window)) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "warning" }, + content: { key: "popup2faCloseMessage" }, + type: "warning", + }); + if (confirmed) { + await BrowserPopupUtils.openCurrentPagePopout(window); + return; + } + } + + await super.ngOnInit(); + } +} diff --git a/apps/browser/src/auth/popup/two-factor-auth.component.ts b/apps/browser/src/auth/popup/two-factor-auth.component.ts new file mode 100644 index 00000000000..27c95321100 --- /dev/null +++ b/apps/browser/src/auth/popup/two-factor-auth.component.ts @@ -0,0 +1,166 @@ +import { CommonModule } from "@angular/common"; +import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; +import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; +import { ActivatedRoute, Router, RouterLink } from "@angular/router"; + +import { TwoFactorAuthAuthenticatorComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-authenticator.component"; +import { TwoFactorAuthWebAuthnComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-webauthn.component"; +import { TwoFactorAuthYubikeyComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-yubikey.component"; +import { TwoFactorAuthComponent as BaseTwoFactorAuthComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth.component"; +import { TwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-options.component"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; +import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; +import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; +import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SyncService } from "@bitwarden/common/platform/sync"; +import { + ButtonModule, + FormFieldModule, + AsyncActionsModule, + CheckboxModule, + DialogModule, + LinkModule, + TypographyModule, + DialogService, + ToastService, +} from "@bitwarden/components"; + +import { + LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, +} from "../../../../../libs/auth/src/common/abstractions"; +import { BrowserApi } from "../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; + +import { TwoFactorAuthDuoComponent } from "./two-factor-auth-duo.component"; +import { TwoFactorAuthEmailComponent } from "./two-factor-auth-email.component"; + +@Component({ + standalone: true, + templateUrl: + "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html", + selector: "app-two-factor-auth", + imports: [ + CommonModule, + JslibModule, + DialogModule, + ButtonModule, + LinkModule, + TypographyModule, + ReactiveFormsModule, + FormFieldModule, + AsyncActionsModule, + RouterLink, + CheckboxModule, + TwoFactorOptionsComponent, + TwoFactorAuthEmailComponent, + TwoFactorAuthAuthenticatorComponent, + TwoFactorAuthYubikeyComponent, + TwoFactorAuthDuoComponent, + TwoFactorAuthWebAuthnComponent, + ], + providers: [I18nPipe], +}) +export class TwoFactorAuthComponent + extends BaseTwoFactorAuthComponent + implements OnInit, OnDestroy +{ + constructor( + protected loginStrategyService: LoginStrategyServiceAbstraction, + protected router: Router, + i18nService: I18nService, + platformUtilsService: PlatformUtilsService, + environmentService: EnvironmentService, + dialogService: DialogService, + protected route: ActivatedRoute, + logService: LogService, + protected twoFactorService: TwoFactorService, + loginEmailService: LoginEmailServiceAbstraction, + userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, + protected ssoLoginService: SsoLoginServiceAbstraction, + protected configService: ConfigService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, + formBuilder: FormBuilder, + @Inject(WINDOW) protected win: Window, + private syncService: SyncService, + private messagingService: MessagingService, + toastService: ToastService, + ) { + super( + loginStrategyService, + router, + i18nService, + platformUtilsService, + environmentService, + dialogService, + route, + logService, + twoFactorService, + loginEmailService, + userDecryptionOptionsService, + ssoLoginService, + configService, + masterPasswordService, + accountService, + formBuilder, + win, + toastService, + ); + super.onSuccessfulLoginTdeNavigate = async () => { + this.win.close(); + }; + this.onSuccessfulLoginNavigate = this.goAfterLogIn; + } + + async ngOnInit(): Promise { + await super.ngOnInit(); + + if (this.route.snapshot.paramMap.has("webAuthnResponse")) { + // WebAuthn fallback response + this.selectedProviderType = TwoFactorProviderType.WebAuthn; + this.token = this.route.snapshot.paramMap.get("webAuthnResponse"); + super.onSuccessfulLogin = async () => { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.syncService.fullSync(true); + this.messagingService.send("reloadPopup"); + window.close(); + }; + this.remember = this.route.snapshot.paramMap.get("remember") === "true"; + await this.submit(); + return; + } + + if (await BrowserPopupUtils.inPopout(this.win)) { + this.selectedProviderType = TwoFactorProviderType.Email; + } + + // WebAuthn prompt appears inside the popup on linux, and requires a larger popup width + // than usual to avoid cutting off the dialog. + if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && (await this.isLinux())) { + document.body.classList.add("linux-webauthn"); + } + } + + async ngOnDestroy() { + if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && (await this.isLinux())) { + document.body.classList.remove("linux-webauthn"); + } + } + + async isLinux() { + return (await BrowserApi.getPlatformInfo()).os === "linux"; + } +} diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts index 98363bc93cc..e9167a5087a 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject } from "@angular/core"; +import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { Subject, Subscription, firstValueFrom } from "rxjs"; import { filter, first, takeUntil } from "rxjs/operators"; @@ -25,7 +25,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { BrowserApi } from "../../platform/browser/browser-api"; import { ZonedMessageListenerService } from "../../platform/browser/zoned-message-listener.service"; @@ -37,7 +37,7 @@ import { closeTwoFactorAuthPopout } from "./utils/auth-popout-window"; selector: "app-two-factor", templateUrl: "two-factor.component.html", }) -export class TwoFactorComponent extends BaseTwoFactorComponent { +export class TwoFactorComponent extends BaseTwoFactorComponent implements OnInit, OnDestroy { private destroy$ = new Subject(); inPopout = BrowserPopupUtils.inPopout(window); @@ -62,6 +62,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { private dialogService: DialogService, masterPasswordService: InternalMasterPasswordServiceAbstraction, accountService: AccountService, + toastService: ToastService, @Inject(WINDOW) protected win: Window, private browserMessagingApi: ZonedMessageListenerService, ) { @@ -84,6 +85,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { configService, masterPasswordService, accountService, + toastService, ); super.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -226,6 +228,15 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { } override async launchDuoFrameless() { + if (this.duoFramelessUrl === null) { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("duoHealthCheckResultsInNullAuthUrlError"), + }); + return; + } + const duoHandOffMessage = { title: this.i18nService.t("youSuccessfullyLoggedIn"), message: this.i18nService.t("youMayCloseThisWindow"), 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/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index e01e2c5c02b..ed9d8e6d84b 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -9,6 +9,7 @@ interface NotificationQueueMessage { type: NotificationQueueMessageTypes; domain: string; tab: chrome.tabs.Tab; + launchTimestamp: number; expires: Date; wasVaultLocked: boolean; } @@ -88,10 +89,9 @@ type NotificationBackgroundExtensionMessage = { tab?: chrome.tabs.Tab; sender?: string; notificationType?: string; + fadeOutNotification?: boolean; }; -type SaveOrUpdateCipherResult = undefined | { error: string }; - type BackgroundMessageParam = { message: NotificationBackgroundExtensionMessage }; type BackgroundSenderParam = { sender: chrome.runtime.MessageSender }; type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam; @@ -100,7 +100,7 @@ type NotificationBackgroundExtensionMessageHandlers = { [key: string]: CallableFunction; unlockCompleted: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgGetFolderData: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; - bgCloseNotificationBar: ({ sender }: BackgroundSenderParam) => Promise; + bgCloseNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgAdjustNotificationBar: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgAddLogin: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; bgChangedPassword: ({ message, sender }: BackgroundOnMessageHandlerParams) => Promise; @@ -129,7 +129,6 @@ export { ChangePasswordMessageData, UnlockVaultMessageData, AddLoginMessageData, - SaveOrUpdateCipherResult, NotificationBackgroundExtensionMessage, NotificationBackgroundExtensionMessageHandlers, }; diff --git a/apps/browser/src/autofill/background/abstractions/overlay-notifications.background.ts b/apps/browser/src/autofill/background/abstractions/overlay-notifications.background.ts new file mode 100644 index 00000000000..0ec6a9ae04a --- /dev/null +++ b/apps/browser/src/autofill/background/abstractions/overlay-notifications.background.ts @@ -0,0 +1,52 @@ +import AutofillPageDetails from "../../models/autofill-page-details"; + +export type NotificationTypeData = { + isVaultLocked?: boolean; + theme?: string; + removeIndividualVault?: boolean; + importType?: string; + launchTimestamp?: number; +}; + +export type WebsiteOriginsWithFields = Map>; + +export type ActiveFormSubmissionRequests = Set; + +export type ModifyLoginCipherFormData = { + uri: string; + username: string; + password: string; + newPassword: string; +}; + +export type ModifyLoginCipherFormDataForTab = Map< + chrome.tabs.Tab["id"], + { uri: string; username: string; password: string; newPassword: string } +>; + +export type OverlayNotificationsExtensionMessage = { + command: string; + uri?: string; + username?: string; + password?: string; + newPassword?: string; + details?: AutofillPageDetails; +}; + +type OverlayNotificationsMessageParams = { message: OverlayNotificationsExtensionMessage }; +type OverlayNotificationSenderParams = { sender: chrome.runtime.MessageSender }; +type OverlayNotificationsMessageHandlersParams = OverlayNotificationsMessageParams & + OverlayNotificationSenderParams; + +export type OverlayNotificationsExtensionMessageHandlers = { + [key: string]: ({ message, sender }: OverlayNotificationsMessageHandlersParams) => any; + formFieldSubmitted: ({ message, sender }: OverlayNotificationsMessageHandlersParams) => void; + collectPageDetailsResponse: ({ + message, + sender, + }: OverlayNotificationsMessageHandlersParams) => Promise; +}; + +export interface OverlayNotificationsBackground { + 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 aa62194af5c..e91a58a84cf 100644 --- a/apps/browser/src/autofill/background/abstractions/overlay.background.ts +++ b/apps/browser/src/autofill/background/abstractions/overlay.background.ts @@ -1,132 +1,256 @@ 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"; import { LockedVaultPendingNotificationsData } from "./notification.background"; -type WebsiteIconData = { +export type PageDetailsForTab = Record< + chrome.runtime.MessageSender["tab"]["id"], + Map +>; + +export type SubFrameOffsetData = { + top: number; + left: number; + url?: string; + frameId?: number; + parentFrameIds?: number[]; +} | null; + +export type SubFrameOffsetsForTab = Record< + chrome.runtime.MessageSender["tab"]["id"], + Map +>; + +export type WebsiteIconData = { imageEnabled: boolean; image: string; fallbackImage: string; icon: string; }; -type OverlayAddNewItemMessage = { - login?: { - uri?: string; - hostname: string; - username: string; - password: string; - }; +export type FocusedFieldData = { + focusedFieldStyles: Partial; + focusedFieldRects: Partial; + filledByCipherType?: CipherType; + tabId?: number; + frameId?: number; + accountCreationFieldType?: string; + showInlineMenuAccountCreation?: boolean; + showPasskeys?: boolean; }; -type OverlayBackgroundExtensionMessage = { - [key: string]: any; +export type InlineMenuElementPosition = { + top: number; + left: number; + width: number; + height: number; +}; + +export type InlineMenuPosition = { + button?: InlineMenuElementPosition; + list?: InlineMenuElementPosition; +}; + +export type NewLoginCipherData = { + uri?: string; + hostname: string; + username: string; + password: string; +}; + +export type NewCardCipherData = { + cardholderName: string; + number: string; + expirationMonth: string; + expirationYear: string; + expirationDate?: string; + cvv: string; +}; + +export type NewIdentityCipherData = { + title: string; + firstName: string; + middleName: string; + lastName: string; + fullName: string; + address1: string; + address2: string; + address3: string; + city: string; + state: string; + postalCode: string; + country: string; + company: string; + phone: string; + email: string; + username: string; +}; + +export type OverlayAddNewItemMessage = { + addNewCipherType?: CipherType; + login?: NewLoginCipherData; + card?: NewCardCipherData; + identity?: NewIdentityCipherData; +}; + +export type CurrentAddNewItemData = OverlayAddNewItemMessage & { + sender: chrome.runtime.MessageSender; +}; + +export type CloseInlineMenuMessage = { + forceCloseInlineMenu?: boolean; + overlayElement?: string; +}; + +export type ToggleInlineMenuHiddenMessage = { + isInlineMenuHidden?: boolean; + setTransparentInlineMenu?: boolean; +}; + +export type OverlayBackgroundExtensionMessage = { command: string; + portKey?: string; tab?: chrome.tabs.Tab; sender?: string; details?: AutofillPageDetails; - overlayElement?: string; - display?: string; + isFieldCurrentlyFocused?: boolean; + isFieldCurrentlyFilling?: boolean; + isVisible?: boolean; + subFrameData?: SubFrameOffsetData; + focusedFieldData?: FocusedFieldData; + styles?: Partial; data?: LockedVaultPendingNotificationsData; -} & OverlayAddNewItemMessage; +} & OverlayAddNewItemMessage & + CloseInlineMenuMessage & + ToggleInlineMenuHiddenMessage; -type OverlayPortMessage = { +export type OverlayPortMessage = { [key: string]: any; command: string; direction?: string; - overlayCipherId?: string; + inlineMenuCipherId?: string; + addNewCipherType?: CipherType; + usePasskey?: boolean; }; -type FocusedFieldData = { - focusedFieldStyles: Partial; - focusedFieldRects: Partial; - tabId?: number; -}; - -type OverlayCipherData = { +export type InlineMenuCipherData = { id: string; name: string; type: CipherType; reprompt: CipherRepromptType; favorite: boolean; - icon: { imageEnabled: boolean; image: string; fallbackImage: string; icon: string }; - login?: { username: string }; + icon: WebsiteIconData; + accountCreationFieldType?: string; + login?: { + username: string; + passkey: { + rpName: string; + userName: string; + } | null; + }; card?: string; + identity?: { + fullName: string; + username?: string; + }; }; -type BackgroundMessageParam = { +export type BuildCipherDataParams = { + inlineMenuCipherId: string; + cipher: CipherView; + showFavicons?: boolean; + showInlineMenuAccountCreation?: boolean; + hasPasskey?: boolean; + identityData?: { fullName: string; username?: string }; +}; + +export type BackgroundMessageParam = { message: OverlayBackgroundExtensionMessage; }; -type BackgroundSenderParam = { +export type BackgroundSenderParam = { sender: chrome.runtime.MessageSender; }; -type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam; +export type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam; -type OverlayBackgroundExtensionMessageHandlers = { +export type OverlayBackgroundExtensionMessageHandlers = { [key: string]: CallableFunction; - openAutofillOverlay: () => void; autofillOverlayElementClosed: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; autofillOverlayAddNewVaultItem: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - getAutofillOverlayVisibility: () => void; - checkAutofillOverlayFocused: () => void; - focusAutofillOverlayList: () => void; - updateAutofillOverlayPosition: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; - updateAutofillOverlayHidden: ({ message }: BackgroundMessageParam) => void; + triggerAutofillOverlayReposition: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + checkIsInlineMenuCiphersPopulated: ({ sender }: BackgroundSenderParam) => void; updateFocusedFieldData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + updateIsFieldCurrentlyFocused: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + checkIsFieldCurrentlyFocused: () => boolean; + updateIsFieldCurrentlyFilling: ({ message }: BackgroundMessageParam) => void; + checkIsFieldCurrentlyFilling: () => boolean; + getAutofillInlineMenuVisibility: () => void; + openAutofillInlineMenu: () => void; + closeAutofillInlineMenu: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + checkAutofillInlineMenuFocused: ({ sender }: BackgroundSenderParam) => void; + focusAutofillInlineMenuList: () => void; + updateAutofillInlineMenuPosition: ({ + message, + sender, + }: BackgroundOnMessageHandlerParams) => Promise; + getAutofillInlineMenuPosition: () => InlineMenuPosition; + updateAutofillInlineMenuElementIsVisibleStatus: ({ + message, + sender, + }: BackgroundOnMessageHandlerParams) => void; + checkIsAutofillInlineMenuButtonVisible: () => void; + checkIsAutofillInlineMenuListVisible: () => void; + getCurrentTabFrameId: ({ sender }: BackgroundSenderParam) => number; + updateSubFrameData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + triggerSubFrameFocusInRebuild: ({ sender }: BackgroundSenderParam) => void; + destroyAutofillInlineMenuListeners: ({ + message, + sender, + }: BackgroundOnMessageHandlerParams) => void; collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; unlockCompleted: ({ message }: BackgroundMessageParam) => void; + doFullSync: () => void; addedCipher: () => void; addEditCipherSubmitted: () => void; editedCipher: () => void; deletedCipher: () => void; + fido2AbortRequest: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; }; -type PortMessageParam = { +export type PortMessageParam = { message: OverlayPortMessage; }; -type PortConnectionParam = { +export type PortConnectionParam = { port: chrome.runtime.Port; }; -type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam; +export type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam; -type OverlayButtonPortMessageHandlers = { +export type InlineMenuButtonPortMessageHandlers = { [key: string]: CallableFunction; - overlayButtonClicked: ({ port }: PortConnectionParam) => void; - closeAutofillOverlay: ({ port }: PortConnectionParam) => void; - forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void; - overlayPageBlurred: () => void; - redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; + triggerDelayedAutofillInlineMenuClosure: () => void; + autofillInlineMenuButtonClicked: ({ port }: PortConnectionParam) => void; + autofillInlineMenuBlurred: () => void; + redirectAutofillInlineMenuFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; + updateAutofillInlineMenuColorScheme: () => void; }; -type OverlayListPortMessageHandlers = { +export type InlineMenuListPortMessageHandlers = { [key: string]: CallableFunction; - checkAutofillOverlayButtonFocused: () => void; - forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void; - overlayPageBlurred: () => void; + checkAutofillInlineMenuButtonFocused: () => void; + autofillInlineMenuBlurred: () => void; unlockVault: ({ port }: PortConnectionParam) => void; - fillSelectedListItem: ({ message, port }: PortOnMessageHandlerParams) => void; - addNewVaultItem: ({ port }: PortConnectionParam) => void; + fillAutofillInlineMenuCipher: ({ message, port }: PortOnMessageHandlerParams) => void; + addNewVaultItem: ({ message, port }: PortOnMessageHandlerParams) => void; viewSelectedCipher: ({ message, port }: PortOnMessageHandlerParams) => void; - redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; + redirectAutofillInlineMenuFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; + updateAutofillInlineMenuListHeight: ({ message, port }: PortOnMessageHandlerParams) => void; }; -interface OverlayBackground { +export interface OverlayBackground { init(): Promise; removePageDetails(tabId: number): void; - updateOverlayCiphers(): void; + updateOverlayCiphers(updateAllCipherTypes?: boolean): Promise; } - -export { - WebsiteIconData, - OverlayBackgroundExtensionMessage, - OverlayPortMessage, - FocusedFieldData, - OverlayCipherData, - OverlayAddNewItemMessage, - OverlayBackgroundExtensionMessageHandlers, - OverlayButtonPortMessageHandlers, - OverlayListPortMessageHandlers, - OverlayBackground, -}; 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/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index 5598c27dd6e..0ede9b96091 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -1,9 +1,11 @@ -import { mock } from "jest-mock-extended"; +import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; +import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; +import { ExtensionCommand } from "@bitwarden/common/autofill/constants"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -11,13 +13,13 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; import { BrowserApi } from "../../platform/browser/browser-api"; -import { DefaultBrowserStateService } from "../../platform/services/default-browser-state.service"; import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum"; import { FormData } from "../services/abstractions/autofill.service"; import AutofillService from "../services/autofill.service"; @@ -46,31 +48,35 @@ describe("NotificationBackground", () => { let notificationBackground: NotificationBackground; const autofillService = mock(); const cipherService = mock(); - const authService = mock(); + let activeAccountStatusMock$: BehaviorSubject; + let authService: MockProxy; const policyService = mock(); const folderService = mock(); - const stateService = mock(); const userNotificationSettingsService = mock(); const domainSettingsService = mock(); const environmentService = mock(); const logService = mock(); const themeStateService = mock(); const configService = mock(); + const accountService = mock(); beforeEach(() => { + activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Locked); + authService = mock(); + authService.activeAccountStatus$ = activeAccountStatusMock$; notificationBackground = new NotificationBackground( autofillService, cipherService, authService, policyService, folderService, - stateService, userNotificationSettingsService, domainSettingsService, environmentService, logService, themeStateService, configService, + accountService, ); }); @@ -89,6 +95,7 @@ describe("NotificationBackground", () => { tab: createChromeTabMock(), expires: new Date(), wasVaultLocked: false, + launchTimestamp: 0, }; const cipherView = notificationBackground["convertAddLoginQueueMessageToCipherView"](message); @@ -124,6 +131,7 @@ describe("NotificationBackground", () => { tab: createChromeTabMock(), expires: new Date(), wasVaultLocked: false, + launchTimestamp: 0, }; const cipherView = notificationBackground["convertAddLoginQueueMessageToCipherView"]( message, @@ -135,8 +143,8 @@ describe("NotificationBackground", () => { }); describe("notification bar extension message handlers", () => { - beforeEach(async () => { - await notificationBackground.init(); + beforeEach(() => { + notificationBackground.init(); }); it("ignores messages whose command does not match the expected handlers", () => { @@ -154,7 +162,7 @@ describe("NotificationBackground", () => { const message: NotificationBackgroundExtensionMessage = { command: "unlockCompleted", data: { - commandToRetry: { message: { command: "autofill_login" } }, + commandToRetry: { message: { command: ExtensionCommand.AutofillLogin } }, } as LockedVaultPendingNotificationsData, }; jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); @@ -220,6 +228,7 @@ describe("NotificationBackground", () => { expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( sender.tab, "closeNotificationBar", + { fadeOutNotification: false }, ); }); }); @@ -247,7 +256,6 @@ describe("NotificationBackground", () => { describe("bgAddLogin message handler", () => { let tab: chrome.tabs.Tab; let sender: chrome.runtime.MessageSender; - let getAuthStatusSpy: jest.SpyInstance; let getEnableAddedLoginPromptSpy: jest.SpyInstance; let getEnableChangedPasswordPromptSpy: jest.SpyInstance; let pushAddLoginToQueueSpy: jest.SpyInstance; @@ -257,7 +265,6 @@ describe("NotificationBackground", () => { beforeEach(() => { tab = createChromeTabMock(); sender = mock({ tab }); - getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus"); getEnableAddedLoginPromptSpy = jest.spyOn( notificationBackground as any, "getEnableAddedLoginPrompt", @@ -279,12 +286,11 @@ describe("NotificationBackground", () => { command: "bgAddLogin", login: { username: "test", password: "password", url: "https://example.com" }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.LoggedOut); + activeAccountStatusMock$.next(AuthenticationStatus.LoggedOut); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).not.toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled(); }); @@ -294,12 +300,11 @@ describe("NotificationBackground", () => { command: "bgAddLogin", login: { username: "test", password: "password", url: "" }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); + activeAccountStatusMock$.next(AuthenticationStatus.Locked); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).not.toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled(); }); @@ -309,13 +314,12 @@ describe("NotificationBackground", () => { command: "bgAddLogin", login: { username: "test", password: "password", url: "https://example.com" }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); + activeAccountStatusMock$.next(AuthenticationStatus.Locked); getEnableAddedLoginPromptSpy.mockReturnValueOnce(false); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).not.toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled(); @@ -327,14 +331,13 @@ describe("NotificationBackground", () => { command: "bgAddLogin", login: { username: "test", password: "password", url: "https://example.com" }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getEnableAddedLoginPromptSpy.mockReturnValueOnce(false); getAllDecryptedForUrlSpy.mockResolvedValueOnce([]); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled(); @@ -346,7 +349,7 @@ describe("NotificationBackground", () => { command: "bgAddLogin", login: { username: "test", password: "password", url: "https://example.com" }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getEnableAddedLoginPromptSpy.mockReturnValueOnce(true); getEnableChangedPasswordPromptSpy.mockReturnValueOnce(false); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ @@ -356,7 +359,6 @@ describe("NotificationBackground", () => { sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); expect(getEnableChangedPasswordPromptSpy).toHaveBeenCalled(); @@ -369,7 +371,7 @@ describe("NotificationBackground", () => { command: "bgAddLogin", login: { username: "test", password: "password", url: "https://example.com" }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getEnableAddedLoginPromptSpy.mockReturnValueOnce(true); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ mock({ login: { username: "test", password: "password" } }), @@ -378,7 +380,6 @@ describe("NotificationBackground", () => { sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getEnableAddedLoginPromptSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).not.toHaveBeenCalled(); @@ -388,13 +389,12 @@ describe("NotificationBackground", () => { it("adds the login to the queue if the user has a locked account", async () => { const login = { username: "test", password: "password", url: "https://example.com" }; const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); + activeAccountStatusMock$.next(AuthenticationStatus.Locked); getEnableAddedLoginPromptSpy.mockReturnValueOnce(true); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).toHaveBeenCalledWith("example.com", login, sender.tab, true); }); @@ -405,7 +405,7 @@ describe("NotificationBackground", () => { url: "https://example.com", } as any; const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getEnableAddedLoginPromptSpy.mockReturnValueOnce(true); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ mock({ login: { username: "anotherTestUsername", password: "password" } }), @@ -414,14 +414,13 @@ describe("NotificationBackground", () => { sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(pushAddLoginToQueueSpy).toHaveBeenCalledWith("example.com", login, sender.tab); }); it("adds a change password message to the queue if the user has changed an existing cipher's password", async () => { const login = { username: "tEsT", password: "password", url: "https://example.com" }; const message: NotificationBackgroundExtensionMessage = { command: "bgAddLogin", login }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getEnableAddedLoginPromptSpy.mockResolvedValueOnce(true); getEnableChangedPasswordPromptSpy.mockResolvedValueOnce(true); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ @@ -446,14 +445,12 @@ describe("NotificationBackground", () => { describe("bgChangedPassword message handler", () => { let tab: chrome.tabs.Tab; let sender: chrome.runtime.MessageSender; - let getAuthStatusSpy: jest.SpyInstance; let pushChangePasswordToQueueSpy: jest.SpyInstance; let getAllDecryptedForUrlSpy: jest.SpyInstance; beforeEach(() => { tab = createChromeTabMock(); sender = mock({ tab }); - getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus"); pushChangePasswordToQueueSpy = jest.spyOn( notificationBackground as any, "pushChangePasswordToQueue", @@ -482,12 +479,11 @@ describe("NotificationBackground", () => { url: "https://example.com", }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); + activeAccountStatusMock$.next(AuthenticationStatus.Locked); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(pushChangePasswordToQueueSpy).toHaveBeenCalledWith( null, "example.com", @@ -506,7 +502,7 @@ describe("NotificationBackground", () => { url: "https://example.com", }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ mock({ login: { username: "test", password: "password" } }), ]); @@ -514,7 +510,6 @@ describe("NotificationBackground", () => { sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); }); @@ -528,7 +523,7 @@ describe("NotificationBackground", () => { url: "https://example.com", }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ mock({ login: { username: "test", password: "password" } }), mock({ login: { username: "test2", password: "password" } }), @@ -537,7 +532,6 @@ describe("NotificationBackground", () => { sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); }); @@ -551,7 +545,7 @@ describe("NotificationBackground", () => { url: "https://example.com", }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ mock({ id: "cipher-id", @@ -578,7 +572,7 @@ describe("NotificationBackground", () => { url: "https://example.com", }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ mock({ login: { username: "test", password: "password" } }), mock({ login: { username: "test2", password: "password" } }), @@ -587,7 +581,6 @@ describe("NotificationBackground", () => { sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(getAllDecryptedForUrlSpy).toHaveBeenCalled(); expect(pushChangePasswordToQueueSpy).not.toHaveBeenCalled(); }); @@ -600,7 +593,7 @@ describe("NotificationBackground", () => { url: "https://example.com", }, }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getAllDecryptedForUrlSpy.mockResolvedValueOnce([ mock({ id: "cipher-id", @@ -656,12 +649,10 @@ describe("NotificationBackground", () => { }); describe("bgSaveCipher message handler", () => { - let getAuthStatusSpy: jest.SpyInstance; let tabSendMessageDataSpy: jest.SpyInstance; let openUnlockPopoutSpy: jest.SpyInstance; beforeEach(() => { - getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus"); tabSendMessageDataSpy = jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); openUnlockPopoutSpy = jest .spyOn(notificationBackground as any, "openUnlockPopout") @@ -675,12 +666,11 @@ describe("NotificationBackground", () => { edit: false, folder: "folder-id", }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); + activeAccountStatusMock$.next(AuthenticationStatus.Locked); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(tabSendMessageDataSpy).toHaveBeenCalledWith( sender.tab, "addToLockedVaultPendingNotifications", @@ -693,6 +683,13 @@ describe("NotificationBackground", () => { }); describe("saveOrUpdateCredentials", () => { + const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({ + id: "testId" as UserId, + email: "test@example.com", + emailVerified: true, + name: "Test User", + }); + let getDecryptedCipherByIdSpy: jest.SpyInstance; let getAllDecryptedForUrlSpy: jest.SpyInstance; let updatePasswordSpy: jest.SpyInstance; @@ -707,7 +704,7 @@ describe("NotificationBackground", () => { let cipherEncryptSpy: jest.SpyInstance; beforeEach(() => { - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Unlocked); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); getDecryptedCipherByIdSpy = jest.spyOn( notificationBackground as any, "getDecryptedCipherById", @@ -729,6 +726,8 @@ describe("NotificationBackground", () => { updateWithServerSpy = jest.spyOn(cipherService, "updateWithServer"); folderExistsSpy = jest.spyOn(notificationBackground as any, "folderExists"); cipherEncryptSpy = jest.spyOn(cipherService, "encrypt"); + + accountService.activeAccount$ = activeAccountSubject; }); it("skips saving the cipher if the notification queue does not have a tab that is related to the sender", async () => { @@ -983,7 +982,7 @@ describe("NotificationBackground", () => { queueMessage, null, ); - expect(cipherEncryptSpy).toHaveBeenCalledWith(cipherView); + expect(cipherEncryptSpy).toHaveBeenCalledWith(cipherView, "testId"); expect(createWithServerSpy).toHaveBeenCalled(); expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { command: "saveCipherAttemptCompleted", @@ -1022,7 +1021,7 @@ describe("NotificationBackground", () => { sendMockExtensionMessage(message, sender); await flushPromises(); - expect(cipherEncryptSpy).toHaveBeenCalledWith(cipherView); + expect(cipherEncryptSpy).toHaveBeenCalledWith(cipherView, "testId"); expect(createWithServerSpy).toThrow(errorMessage); expect(tabSendMessageSpy).not.toHaveBeenCalledWith(sender.tab, { command: "addedCipher", @@ -1203,11 +1202,9 @@ describe("NotificationBackground", () => { }); describe("bgUnlockPopoutOpened message handler", () => { - let getAuthStatusSpy: jest.SpyInstance; let pushUnlockVaultToQueueSpy: jest.SpyInstance; beforeEach(() => { - getAuthStatusSpy = jest.spyOn(authService, "getAuthStatus"); pushUnlockVaultToQueueSpy = jest.spyOn( notificationBackground as any, "pushUnlockVaultToQueue", @@ -1225,7 +1222,6 @@ describe("NotificationBackground", () => { sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).not.toHaveBeenCalled(); expect(pushUnlockVaultToQueueSpy).not.toHaveBeenCalled(); }); @@ -1235,12 +1231,11 @@ describe("NotificationBackground", () => { const message: NotificationBackgroundExtensionMessage = { command: "bgUnlockPopoutOpened", }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.LoggedOut); + activeAccountStatusMock$.next(AuthenticationStatus.LoggedOut); sendMockExtensionMessage(message, sender); await flushPromises(); - expect(getAuthStatusSpy).toHaveBeenCalled(); expect(pushUnlockVaultToQueueSpy).not.toHaveBeenCalled(); }); @@ -1250,7 +1245,7 @@ describe("NotificationBackground", () => { const message: NotificationBackgroundExtensionMessage = { command: "bgUnlockPopoutOpened", }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); + activeAccountStatusMock$.next(AuthenticationStatus.Locked); notificationBackground["notificationQueue"] = [mock()]; sendMockExtensionMessage(message, sender); @@ -1265,7 +1260,7 @@ describe("NotificationBackground", () => { const message: NotificationBackgroundExtensionMessage = { command: "bgUnlockPopoutOpened", }; - getAuthStatusSpy.mockResolvedValueOnce(AuthenticationStatus.Locked); + activeAccountStatusMock$.next(AuthenticationStatus.Locked); sendMockExtensionMessage(message, sender); await flushPromises(); diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 9b65e4db0b2..683e3d8f581 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -1,10 +1,15 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { NOTIFICATION_BAR_LIFESPAN_MS } from "@bitwarden/common/autofill/constants"; +import { + ExtensionCommand, + ExtensionCommandType, + NOTIFICATION_BAR_LIFESPAN_MS, +} from "@bitwarden/common/autofill/constants"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; @@ -23,7 +28,6 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window"; import { BrowserApi } from "../../platform/browser/browser-api"; -import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service"; import { openAddEditVaultItemPopout } from "../../vault/popup/utils/vault-popout-window"; import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum"; import { AutofillService } from "../services/abstractions/autofill.service"; @@ -40,16 +44,23 @@ import { NotificationBackgroundExtensionMessage, NotificationBackgroundExtensionMessageHandlers, } from "./abstractions/notification.background"; +import { NotificationTypeData } from "./abstractions/overlay-notifications.background"; import { OverlayBackgroundExtensionMessage } from "./abstractions/overlay.background"; export default class NotificationBackground { private openUnlockPopout = openUnlockPopout; private openAddEditVaultItemPopout = openAddEditVaultItemPopout; private notificationQueue: NotificationQueueMessageItem[] = []; + private allowedRetryCommands: Set = new Set([ + ExtensionCommand.AutofillLogin, + ExtensionCommand.AutofillCard, + ExtensionCommand.AutofillIdentity, + ]); private readonly extensionMessageHandlers: NotificationBackgroundExtensionMessageHandlers = { unlockCompleted: ({ message, sender }) => this.handleUnlockCompleted(message, sender), bgGetFolderData: () => this.getFolderData(), - bgCloseNotificationBar: ({ sender }) => this.handleCloseNotificationBarMessage(sender), + bgCloseNotificationBar: ({ message, sender }) => + this.handleCloseNotificationBarMessage(message, sender), bgAdjustNotificationBar: ({ message, sender }) => this.handleAdjustNotificationBarMessage(message, sender), bgAddLogin: ({ message, sender }) => this.addLogin(message, sender), @@ -76,16 +87,16 @@ export default class NotificationBackground { private authService: AuthService, private policyService: PolicyService, private folderService: FolderService, - private stateService: BrowserStateService, private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, private domainSettingsService: DomainSettingsService, private environmentService: EnvironmentService, private logService: LogService, private themeStateService: ThemeStateService, private configService: ConfigService, + private accountService: AccountService, ) {} - async init() { + init() { if (chrome.runtime == null) { return; } @@ -123,6 +134,10 @@ export default class NotificationBackground { return await firstValueFrom(this.configService.serverConfig$); } + private async getAuthStatus() { + return await firstValueFrom(this.authService.activeAccountStatus$); + } + /** * Checks the notification queue for any messages that need to be sent to the * specified tab. If no tab is specified, the current tab will be used. @@ -177,9 +192,10 @@ export default class NotificationBackground { ) { const notificationType = notificationQueueMessage.type; - const typeData: Record = { + const typeData: NotificationTypeData = { isVaultLocked: notificationQueueMessage.wasVaultLocked, theme: await firstValueFrom(this.themeStateService.selectedTheme$), + launchTimestamp: notificationQueueMessage.launchTimestamp, }; switch (notificationType) { @@ -221,11 +237,11 @@ export default class NotificationBackground { * @param message - The message to add to the queue * @param sender - The contextual sender of the message */ - private async addLogin( + async addLogin( message: NotificationBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ) { - const authStatus = await this.authService.getAuthStatus(); + const authStatus = await this.getAuthStatus(); if (authStatus === AuthenticationStatus.LoggedOut) { return; } @@ -280,6 +296,7 @@ export default class NotificationBackground { ) { // remove any old messages for this tab this.removeTabFromNotificationQueue(tab); + const launchTimestamp = new Date().getTime(); const message: AddLoginQueueMessage = { type: NotificationQueueMessageType.AddLogin, username: loginInfo.username, @@ -287,7 +304,8 @@ export default class NotificationBackground { domain: loginDomain, uri: loginInfo.url, tab: tab, - expires: new Date(new Date().getTime() + NOTIFICATION_BAR_LIFESPAN_MS), + launchTimestamp, + expires: new Date(launchTimestamp + NOTIFICATION_BAR_LIFESPAN_MS), wasVaultLocked: isVaultLocked, }; this.notificationQueue.push(message); @@ -301,7 +319,7 @@ export default class NotificationBackground { * @param message - The message to add to the queue * @param sender - The contextual sender of the message */ - private async changedPassword( + async changedPassword( message: NotificationBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ) { @@ -311,7 +329,7 @@ export default class NotificationBackground { return; } - if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) { + if ((await this.getAuthStatus()) < AuthenticationStatus.Unlocked) { await this.pushChangePasswordToQueue( null, loginDomain, @@ -371,7 +389,7 @@ export default class NotificationBackground { return; } - const currentAuthStatus = await this.authService.getAuthStatus(); + const currentAuthStatus = await this.getAuthStatus(); if (currentAuthStatus !== AuthenticationStatus.Locked || this.notificationQueue.length) { return; } @@ -390,7 +408,7 @@ export default class NotificationBackground { * @param importType - The type of import that is being requested */ async requestFilelessImport(tab: chrome.tabs.Tab, importType: string) { - const currentAuthStatus = await this.authService.getAuthStatus(); + const currentAuthStatus = await this.getAuthStatus(); if (currentAuthStatus !== AuthenticationStatus.Unlocked || this.notificationQueue.length) { return; } @@ -410,13 +428,15 @@ export default class NotificationBackground { ) { // remove any old messages for this tab this.removeTabFromNotificationQueue(tab); + const launchTimestamp = new Date().getTime(); const message: AddChangePasswordQueueMessage = { type: NotificationQueueMessageType.ChangePassword, cipherId: cipherId, newPassword: newPassword, domain: loginDomain, tab: tab, - expires: new Date(new Date().getTime() + NOTIFICATION_BAR_LIFESPAN_MS), + launchTimestamp, + expires: new Date(launchTimestamp + NOTIFICATION_BAR_LIFESPAN_MS), wasVaultLocked: isVaultLocked, }; this.notificationQueue.push(message); @@ -425,11 +445,13 @@ export default class NotificationBackground { private async pushUnlockVaultToQueue(loginDomain: string, tab: chrome.tabs.Tab) { this.removeTabFromNotificationQueue(tab); + const launchTimestamp = new Date().getTime(); const message: AddUnlockVaultQueueMessage = { type: NotificationQueueMessageType.UnlockVault, domain: loginDomain, tab: tab, - expires: new Date(new Date().getTime() + 0.5 * 60000), // 30 seconds + launchTimestamp, + expires: new Date(launchTimestamp + 0.5 * 60000), // 30 seconds wasVaultLocked: true, }; await this.sendNotificationQueueMessage(tab, message); @@ -450,11 +472,13 @@ export default class NotificationBackground { importType?: string, ) { this.removeTabFromNotificationQueue(tab); + const launchTimestamp = new Date().getTime(); const message: AddRequestFilelessImportQueueMessage = { type: NotificationQueueMessageType.RequestFilelessImport, domain: loginDomain, tab, - expires: new Date(new Date().getTime() + 0.5 * 60000), // 30 seconds + launchTimestamp, + expires: new Date(launchTimestamp + 0.5 * 60000), // 30 seconds wasVaultLocked: false, importType, }; @@ -475,7 +499,7 @@ export default class NotificationBackground { message: NotificationBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ) { - if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) { + if ((await this.getAuthStatus()) < AuthenticationStatus.Unlocked) { await BrowserApi.tabSendMessageData(sender.tab, "addToLockedVaultPendingNotifications", { commandToRetry: { message: { @@ -549,7 +573,11 @@ export default class NotificationBackground { return; } - const cipher = await this.cipherService.encrypt(newCipher); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + const cipher = await this.cipherService.encrypt(newCipher, activeUserId); try { await this.cipherService.createWithServer(cipher); await BrowserApi.tabSendMessage(tab, { command: "saveCipherAttemptCompleted" }); @@ -587,7 +615,11 @@ export default class NotificationBackground { return; } - const cipher = await this.cipherService.encrypt(cipherView); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + const cipher = await this.cipherService.encrypt(cipherView, activeUserId); try { // We've only updated the password, no need to broadcast editedCipher message await this.cipherService.updateWithServer(cipher); @@ -627,7 +659,13 @@ export default class NotificationBackground { private async getDecryptedCipherById(cipherId: string) { const cipher = await this.cipherService.get(cipherId); if (cipher != null && cipher.type === CipherType.Login) { - return await cipher.decrypt(await this.cipherService.getKeyForCipherKeyDecryption(cipher)); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + return await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), + ); } return null; } @@ -691,8 +729,8 @@ export default class NotificationBackground { sender: chrome.runtime.MessageSender, ): Promise { const messageData = message.data as LockedVaultPendingNotificationsData; - const retryCommand = messageData.commandToRetry.message.command; - if (retryCommand === "autofill_login") { + const retryCommand = messageData.commandToRetry.message.command as ExtensionCommandType; + if (this.allowedRetryCommands.has(retryCommand)) { await BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar"); } @@ -713,10 +751,16 @@ export default class NotificationBackground { * Sends a message back to the sender tab which * triggers closure of the notification bar. * + * @param message - The extension message * @param sender - The contextual sender of the message */ - private async handleCloseNotificationBarMessage(sender: chrome.runtime.MessageSender) { - await BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar"); + private async handleCloseNotificationBarMessage( + message: NotificationBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + await BrowserApi.tabSendMessageData(sender.tab, "closeNotificationBar", { + fadeOutNotification: !!message.fadeOutNotification, + }); } /** @@ -772,12 +816,12 @@ export default class NotificationBackground { ) => { const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command]; if (!handler) { - return; + return null; } const messageResponse = handler({ message, sender }); - if (!messageResponse) { - return; + if (typeof messageResponse === "undefined") { + return null; } Promise.resolve(messageResponse) diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts b/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts new file mode 100644 index 00000000000..d694438c00f --- /dev/null +++ b/apps/browser/src/autofill/background/overlay-notifications.background.spec.ts @@ -0,0 +1,548 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { EnvironmentServerConfigData } from "@bitwarden/common/platform/models/data/server-config.data"; + +import { BrowserApi } from "../../platform/browser/browser-api"; +import AutofillField from "../models/autofill-field"; +import AutofillPageDetails from "../models/autofill-page-details"; +import { + flushPromises, + sendMockExtensionMessage, + triggerTabOnRemovedEvent, + triggerTabOnUpdatedEvent, + triggerWebNavigationOnCompletedEvent, + triggerWebRequestOnBeforeRequestEvent, + triggerWebRequestOnCompletedEvent, +} from "../spec/testing-utils"; + +import NotificationBackground from "./notification.background"; +import { OverlayNotificationsBackground } from "./overlay-notifications.background"; + +describe("OverlayNotificationsBackground", () => { + let logService: MockProxy; + let configService: MockProxy; + let notificationBackground: NotificationBackground; + let getEnableChangedPasswordPromptSpy: jest.SpyInstance; + let getEnableAddedLoginPromptSpy: jest.SpyInstance; + let overlayNotificationsBackground: OverlayNotificationsBackground; + + beforeEach(async () => { + jest.useFakeTimers(); + logService = mock(); + configService = mock(); + notificationBackground = mock(); + getEnableChangedPasswordPromptSpy = jest + .spyOn(notificationBackground, "getEnableChangedPasswordPrompt") + .mockResolvedValue(true); + getEnableAddedLoginPromptSpy = jest + .spyOn(notificationBackground, "getEnableAddedLoginPrompt") + .mockResolvedValue(true); + overlayNotificationsBackground = new OverlayNotificationsBackground( + logService, + configService, + notificationBackground, + ); + configService.getFeatureFlag.mockResolvedValue(true); + await overlayNotificationsBackground.init(); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.clearAllTimers(); + }); + + describe("setting up the form submission listeners", () => { + let fields: MockProxy[]; + let details: MockProxy; + + beforeEach(() => { + fields = [mock(), mock(), mock()]; + details = mock({ fields }); + }); + + describe("skipping setting up the web request listeners", () => { + it("skips setting up listeners when the notification bar is disabled", async () => { + getEnableChangedPasswordPromptSpy.mockResolvedValue(false); + getEnableAddedLoginPromptSpy.mockResolvedValue(false); + + sendMockExtensionMessage({ + command: "collectPageDetailsResponse", + details, + }); + await flushPromises(); + + expect(chrome.webRequest.onCompleted.addListener).not.toHaveBeenCalled(); + }); + + describe("when the sender is from an excluded domain", () => { + const senderHost = "example.com"; + const senderUrl = `https://${senderHost}`; + + beforeEach(() => { + jest.spyOn(notificationBackground, "getExcludedDomains").mockResolvedValue({ + [senderHost]: null, + }); + }); + + it("skips setting up listeners when the sender is the user's vault", async () => { + const vault = "https://vault.bitwarden.com"; + const sender = mock({ origin: vault }); + jest + .spyOn(notificationBackground, "getActiveUserServerConfig") + .mockResolvedValue( + mock({ environment: mock({ vault }) }), + ); + + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + + expect(chrome.webRequest.onCompleted.addListener).not.toHaveBeenCalled(); + }); + + it("skips setting up listeners when the sender is an excluded domain", async () => { + const sender = mock({ origin: senderUrl }); + + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + + expect(chrome.webRequest.onCompleted.addListener).not.toHaveBeenCalled(); + }); + + it("skips setting up listeners when the sender contains a malformed origin", async () => { + const senderOrigin = "-_-!..exampwle.com"; + const sender = mock({ origin: senderOrigin }); + + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + + expect(chrome.webRequest.onCompleted.addListener).not.toHaveBeenCalled(); + }); + }); + + it("skips setting up listeners when the sender tab does not contain page details fields", async () => { + const sender = mock({ tab: { id: 1 } }); + details.fields = []; + + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + + expect(chrome.webRequest.onCompleted.addListener).not.toHaveBeenCalled(); + }); + }); + + it("sets up the web request listeners", async () => { + const sender = mock({ + tab: { id: 1 }, + url: "example.com", + }); + + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + + expect(chrome.webRequest.onCompleted.addListener).toHaveBeenCalled(); + }); + + it("skips setting up duplicate listeners when the website origin has been previously encountered with fields", async () => { + const sender = mock({ + tab: { id: 1 }, + url: "example.com", + }); + + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + sendMockExtensionMessage({ command: "collectPageDetailsResponse", details }, sender); + await flushPromises(); + + expect(chrome.webRequest.onCompleted.addListener).toHaveBeenCalledTimes(1); + }); + }); + + describe("storing the modified login form data", () => { + const sender = mock({ tab: { id: 1 } }); + + it("stores the modified login cipher form data", async () => { + sendMockExtensionMessage( + { + command: "formFieldSubmitted", + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }, + sender, + ); + await flushPromises(); + + expect( + overlayNotificationsBackground["modifyLoginCipherFormData"].get(sender.tab.id), + ).toEqual({ + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }); + }); + + it("clears the modified login cipher form data after 5 seconds", () => { + sendMockExtensionMessage( + { + command: "formFieldSubmitted", + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }, + sender, + ); + + jest.advanceTimersByTime(CLEAR_NOTIFICATION_LOGIN_DATA_DURATION); + + expect(overlayNotificationsBackground["modifyLoginCipherFormData"].size).toBe(0); + }); + + it("attempts to store the modified login cipher form data within the onBeforeRequest listener when the data is not captured through a submit button click event", async () => { + const pageDetails = mock({ fields: [mock()] }); + const tab = mock({ id: sender.tab.id }); + jest.spyOn(BrowserApi, "getTab").mockResolvedValueOnce(tab); + const response = { + command: "formFieldSubmitted", + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }; + jest.spyOn(BrowserApi, "tabSendMessage").mockResolvedValueOnce(response); + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: pageDetails }, + sender, + ); + await flushPromises(); + + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: "https://example.com", + tabId: sender.tab.id, + method: "POST", + requestId: "123345", + }), + ); + await flushPromises(); + + expect( + overlayNotificationsBackground["modifyLoginCipherFormData"].get(sender.tab.id), + ).toEqual({ + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }); + }); + }); + + describe("web request listeners", () => { + let sender: MockProxy; + const pageDetails = mock({ fields: [mock()] }); + let notificationChangedPasswordSpy: jest.SpyInstance; + let notificationAddLoginSpy: jest.SpyInstance; + + beforeEach(async () => { + sender = mock({ + tab: { id: 1 }, + url: "https://example.com", + }); + notificationChangedPasswordSpy = jest.spyOn(notificationBackground, "changedPassword"); + notificationAddLoginSpy = jest.spyOn(notificationBackground, "addLogin"); + + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: pageDetails }, + sender, + ); + await flushPromises(); + }); + + describe("ignored web requests", () => { + it("ignores requests from urls that do not start with a valid protocol", async () => { + sender.url = "chrome-extension://extension-id"; + + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + }), + ); + + expect(overlayNotificationsBackground["activeFormSubmissionRequests"].size).toBe(0); + }); + + it("ignores requests from urls that do not have a valid tabId", async () => { + sender.tab = mock({ id: -1 }); + + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + }), + ); + + expect(overlayNotificationsBackground["activeFormSubmissionRequests"].size).toBe(0); + }); + + it("ignores requests from urls that do not have a valid request method", async () => { + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "GET", + }), + ); + + expect(overlayNotificationsBackground["activeFormSubmissionRequests"].size).toBe(0); + }); + + it("ignores requests that are not part of an active form submission", async () => { + triggerWebRequestOnCompletedEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId: "123345", + }), + ); + + expect(notificationChangedPasswordSpy).not.toHaveBeenCalled(); + expect(notificationAddLoginSpy).not.toHaveBeenCalled(); + }); + + it("ignores requests for tabs that do not contain stored login data", async () => { + const requestId = "123345"; + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId, + }), + ); + await flushPromises(); + + triggerWebRequestOnCompletedEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId, + }), + ); + + expect(notificationChangedPasswordSpy).not.toHaveBeenCalled(); + expect(notificationAddLoginSpy).not.toHaveBeenCalled(); + }); + }); + + describe("web requests that trigger notifications", () => { + const requestId = "123345"; + + beforeEach(async () => { + sendMockExtensionMessage( + { + command: "formFieldSubmitted", + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }, + sender, + ); + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId, + }), + ); + await flushPromises(); + }); + + it("waits for the tab's navigation to complete using the web navigation API before initializing the notification", async () => { + chrome.tabs.get = jest.fn().mockImplementationOnce((tabId, callback) => { + callback( + mock({ + status: "loading", + url: sender.url, + }), + ); + }); + triggerWebRequestOnCompletedEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId, + }), + ); + await flushPromises(); + + chrome.tabs.get = jest.fn().mockImplementationOnce((tabId, callback) => { + callback( + mock({ + status: "complete", + url: sender.url, + }), + ); + }); + triggerWebNavigationOnCompletedEvent( + mock({ + tabId: sender.tab.id, + url: sender.url, + }), + ); + await flushPromises(); + + expect(notificationAddLoginSpy).toHaveBeenCalled(); + }); + + it("initializes the notification immediately when the tab's navigation is complete", async () => { + sendMockExtensionMessage( + { + command: "formFieldSubmitted", + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }, + sender, + ); + await flushPromises(); + chrome.tabs.get = jest.fn().mockImplementationOnce((tabId, callback) => { + callback( + mock({ + status: "complete", + url: sender.url, + }), + ); + }); + + triggerWebRequestOnCompletedEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId, + }), + ); + await flushPromises(); + + expect(notificationAddLoginSpy).toHaveBeenCalled(); + }); + + it("triggers the notification on the beforeRequest listener when a post-submission redirection is encountered", async () => { + sender.tab = mock({ id: 4 }); + sendMockExtensionMessage( + { + command: "formFieldSubmitted", + uri: "example.com", + username: "", + password: "password", + newPassword: "newPassword", + }, + sender, + ); + await flushPromises(); + chrome.tabs.get = jest.fn().mockImplementation((tabId, callback) => { + callback( + mock({ + status: "complete", + url: sender.url, + }), + ); + }); + + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId, + }), + ); + await flushPromises(); + + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: "https://example.com/redirect", + tabId: sender.tab.id, + method: "GET", + requestId, + }), + ); + await flushPromises(); + + expect(notificationChangedPasswordSpy).toHaveBeenCalled(); + }); + }); + }); + + describe("tab listeners", () => { + let sender: MockProxy; + const pageDetails = mock({ fields: [mock()] }); + const requestId = "123345"; + + beforeEach(async () => { + sender = mock({ + tab: { id: 1 }, + url: "https://example.com", + }); + + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: pageDetails }, + sender, + ); + await flushPromises(); + triggerWebRequestOnBeforeRequestEvent( + mock({ + url: sender.url, + tabId: sender.tab.id, + method: "POST", + requestId, + }), + ); + await flushPromises(); + sendMockExtensionMessage( + { + command: "formFieldSubmitted", + uri: "example.com", + username: "username", + password: "password", + newPassword: "newPassword", + }, + sender, + ); + await flushPromises(); + }); + + it("clears all associated data with a removed tab", () => { + triggerTabOnRemovedEvent(sender.tab.id, mock()); + + expect(overlayNotificationsBackground["websiteOriginsWithFields"].size).toBe(0); + }); + + it("clears all associated data with a tab that is entering a `loading` state", () => { + triggerTabOnUpdatedEvent( + sender.tab.id, + mock({ status: "loading" }), + mock({ status: "loading" }), + ); + + expect(overlayNotificationsBackground["websiteOriginsWithFields"].size).toBe(0); + }); + }); +}); diff --git a/apps/browser/src/autofill/background/overlay-notifications.background.ts b/apps/browser/src/autofill/background/overlay-notifications.background.ts new file mode 100644 index 00000000000..e252bdcc4af --- /dev/null +++ b/apps/browser/src/autofill/background/overlay-notifications.background.ts @@ -0,0 +1,557 @@ +import { Subject, switchMap, timer } from "rxjs"; + +import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants"; +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 { BrowserApi } from "../../platform/browser/browser-api"; + +import { + ActiveFormSubmissionRequests, + ModifyLoginCipherFormData, + ModifyLoginCipherFormDataForTab, + OverlayNotificationsBackground as OverlayNotificationsBackgroundInterface, + OverlayNotificationsExtensionMessage, + OverlayNotificationsExtensionMessageHandlers, + WebsiteOriginsWithFields, +} from "./abstractions/overlay-notifications.background"; +import NotificationBackground from "./notification.background"; + +export class OverlayNotificationsBackground implements OverlayNotificationsBackgroundInterface { + private websiteOriginsWithFields: WebsiteOriginsWithFields = new Map(); + private activeFormSubmissionRequests: ActiveFormSubmissionRequests = new Set(); + private modifyLoginCipherFormData: ModifyLoginCipherFormDataForTab = new Map(); + private clearLoginCipherFormDataSubject: Subject = new Subject(); + private readonly formSubmissionRequestMethods: Set = new Set(["POST", "PUT", "PATCH"]); + private readonly extensionMessageHandlers: OverlayNotificationsExtensionMessageHandlers = { + formFieldSubmitted: ({ message, sender }) => this.storeModifiedLoginFormData(message, sender), + collectPageDetailsResponse: ({ message, sender }) => + this.handleCollectPageDetailsResponse(message, sender), + }; + + constructor( + private logService: LogService, + private configService: ConfigService, + private notificationBackground: NotificationBackground, + ) {} + + /** + * Initialize the overlay notifications background service. + */ + async init() { + const featureFlagActive = await this.configService.getFeatureFlag( + FeatureFlag.NotificationBarAddLoginImprovements, + ); + if (!featureFlagActive) { + return; + } + + this.setupExtensionListeners(); + this.clearLoginCipherFormDataSubject + .pipe(switchMap(() => timer(CLEAR_NOTIFICATION_LOGIN_DATA_DURATION))) + .subscribe(() => this.modifyLoginCipherFormData.clear()); + } + + /** + * Handles the response from the content script with the page details. Triggers an initialization + * of the add login or change password notification if the conditions are met. + * + * @param message - The message from the content script + * @param sender - The sender of the message + */ + private async handleCollectPageDetailsResponse( + message: OverlayNotificationsExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + if (await this.shouldInitAddLoginOrChangePasswordNotification(message, sender)) { + this.websiteOriginsWithFields.set(sender.tab.id, this.getSenderUrlMatchPatterns(sender)); + this.setupWebRequestsListeners(); + } + } + + /** + * Determines if the add login or change password notification should be initialized. This depends + * on whether the user has enabled the notification, the sender is not from an excluded domain, the + * tab's page details contains fillable fields, and the website origin has not been previously stored. + * + * @param message - The message from the content script + * @param sender - The sender of the message + */ + private async shouldInitAddLoginOrChangePasswordNotification( + message: OverlayNotificationsExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + return ( + (await this.isAddLoginOrChangePasswordNotificationEnabled()) && + !(await this.isSenderFromExcludedDomain(sender)) && + message.details?.fields?.length > 0 && + !this.websiteOriginsWithFields.has(sender.tab.id) + ); + } + + /** + * Determines if the add login or change password notification is enabled. + * This is based on the user's settings for the notification. + */ + private async isAddLoginOrChangePasswordNotificationEnabled() { + return ( + (await this.notificationBackground.getEnableChangedPasswordPrompt()) || + (await this.notificationBackground.getEnableAddedLoginPrompt()) + ); + } + + /** + * Returns the match patterns for the sender's URL. This is used to filter out + * the web requests that are not from the sender's tab. + * + * @param sender - The sender of the message + */ + private getSenderUrlMatchPatterns(sender: chrome.runtime.MessageSender) { + return new Set([ + ...this.generateMatchPatterns(sender.url), + ...this.generateMatchPatterns(sender.tab.url), + ]); + } + + /** + * Generates the origin and subdomain match patterns for the URL. + * + * @param url - The URL of the tab + */ + private generateMatchPatterns(url: string): string[] { + try { + if (!url.startsWith("http")) { + url = `https://${url}`; + } + + const originMatchPattern = `${new URL(url).origin}/*`; + + const parsedUrl = new URL(url); + const splitHost = parsedUrl.hostname.split("."); + const domain = splitHost.slice(-2).join("."); + const subDomainMatchPattern = `${parsedUrl.protocol}//*.${domain}/*`; + + return [originMatchPattern, subDomainMatchPattern]; + } catch { + return []; + } + } + + /** + * Stores the login form data that was modified by the user in the content script. This data is + * used to trigger the add login or change password notification when the form is submitted. + * + * @param message - The message from the content script + * @param sender - The sender of the message + */ + private storeModifiedLoginFormData = ( + message: OverlayNotificationsExtensionMessage, + sender: chrome.runtime.MessageSender, + ) => { + const { uri, username, password, newPassword } = message; + if (!username && !password && !newPassword) { + return; + } + + this.clearLoginCipherFormDataSubject.next(); + const formData = { uri, username, password, newPassword }; + + const existingModifyLoginData = this.modifyLoginCipherFormData.get(sender.tab.id); + if (existingModifyLoginData) { + formData.username = formData.username || existingModifyLoginData.username; + formData.password = formData.password || existingModifyLoginData.password; + formData.newPassword = formData.newPassword || existingModifyLoginData.newPassword; + } + + this.modifyLoginCipherFormData.set(sender.tab.id, formData); + }; + + /** + * Determines if the sender of the message is from an excluded domain. This is used to prevent the + * add login or change password notification from being triggered on the user's vault domain or + * other excluded domains. + * + * @param sender - The sender of the message + */ + private async isSenderFromExcludedDomain(sender: chrome.runtime.MessageSender): Promise { + try { + const senderOrigin = sender.origin; + const serverConfig = await this.notificationBackground.getActiveUserServerConfig(); + const activeUserVault = serverConfig?.environment?.vault; + if (activeUserVault === senderOrigin) { + return true; + } + + const excludedDomains = await this.notificationBackground.getExcludedDomains(); + if (!excludedDomains) { + return false; + } + + const senderDomain = new URL(senderOrigin).hostname; + return excludedDomains[senderDomain] !== undefined; + } catch { + return true; + } + } + + /** + * Removes and resets the onBeforeRequest and onCompleted listeners for web requests. This ensures + * that we are only listening for form submission requests on the tabs that have fillable form fields. + */ + private setupWebRequestsListeners() { + chrome.webRequest.onBeforeRequest.removeListener(this.handleOnBeforeRequestEvent); + chrome.webRequest.onCompleted.removeListener(this.handleOnCompletedRequestEvent); + if (this.websiteOriginsWithFields.size) { + const requestFilter: chrome.webRequest.RequestFilter = this.generateRequestFilter(); + chrome.webRequest.onBeforeRequest.addListener(this.handleOnBeforeRequestEvent, requestFilter); + chrome.webRequest.onCompleted.addListener(this.handleOnCompletedRequestEvent, requestFilter); + } + } + + /** + * Generates the request filter for the web requests. This is used to filter out the web requests + * that are not from the tabs that have fillable form fields. + */ + private generateRequestFilter(): chrome.webRequest.RequestFilter { + const websiteOrigins = Array.from(this.websiteOriginsWithFields.values()); + const urls: string[] = []; + websiteOrigins.forEach((origins) => urls.push(...origins)); + return { + urls, + types: ["main_frame", "sub_frame", "xmlhttprequest"], + }; + } + + /** + * Handles the onBeforeRequest event for web requests. This is used to ensures that the following + * onCompleted event is only triggered for form submission requests. + * + * @param details - The details of the web request + */ + private handleOnBeforeRequestEvent = (details: chrome.webRequest.WebRequestDetails) => { + if (this.isPostSubmissionFormRedirection(details)) { + this.setupNotificationInitTrigger( + details.tabId, + details.requestId, + this.modifyLoginCipherFormData.get(details.tabId), + ).catch((error) => this.logService.error(error)); + + return; + } + + if (!this.isValidFormSubmissionRequest(details)) { + return; + } + + const { requestId, tabId, frameId } = details; + this.activeFormSubmissionRequests.add(requestId); + + if (this.notificationDataIncompleteOnBeforeRequest(tabId)) { + this.getFormFieldDataFromTab(tabId, frameId).catch((error) => this.logService.error(error)); + } + }; + + /** + * Captures the modified login form data if the tab contains incomplete data. This is used as + * a redundancy to ensure that the modified login form data is captured in cases where the form + * is split into multiple parts. + * + * @param tabId - The id of the tab + */ + private notificationDataIncompleteOnBeforeRequest = (tabId: number) => { + const modifyLoginData = this.modifyLoginCipherFormData.get(tabId); + return ( + !modifyLoginData || + !this.shouldTriggerAddLoginNotification(modifyLoginData) || + !this.shouldTriggerChangePasswordNotification(modifyLoginData) + ); + }; + + /** + * Determines whether the request is happening after a form submission. This is identified by a GET + * request that is triggered after a form submission POST request from the same request id. If + * this is the case, and the modified login form data is available, the add login or change password + * notification is triggered. + * + * @param details - The details of the web request + */ + private isPostSubmissionFormRedirection = (details: chrome.webRequest.WebRequestDetails) => { + return ( + details.method?.toUpperCase() === "GET" && + this.activeFormSubmissionRequests.has(details.requestId) && + this.modifyLoginCipherFormData.has(details.tabId) + ); + }; + + /** + * Determines if the web request is a valid form submission request. A valid web request + * is a POST, PUT, or PATCH request that is not from an invalid host. + * + * @param details - The details of the web request + */ + private isValidFormSubmissionRequest = (details: chrome.webRequest.WebRequestDetails) => { + return ( + !this.requestHostIsInvalid(details) && + this.formSubmissionRequestMethods.has(details.method?.toUpperCase()) + ); + }; + + /** + * Retrieves the form field data from the tab. This is used to get the modified login form data + * in cases where the submit button is not clicked, but the form is submitted through other means. + * + * @param tabId - The senders tab id + * @param frameId - The frame where the form is located + */ + private getFormFieldDataFromTab = async (tabId: number, frameId: number) => { + const tab = await BrowserApi.getTab(tabId); + if (!tab) { + return; + } + + const response = (await BrowserApi.tabSendMessage( + tab, + { command: "getFormFieldDataForNotification" }, + { frameId }, + )) as OverlayNotificationsExtensionMessage; + if (response) { + this.storeModifiedLoginFormData(response, { tab }); + } + }; + + /** + * Handles the onCompleted event for web requests. This is used to trigger the add login or change + * password notification when a form submission request is completed. + * + * @param details - The details of the web response + */ + private handleOnCompletedRequestEvent = async (details: chrome.webRequest.WebResponseDetails) => { + if ( + this.requestHostIsInvalid(details) || + this.isInvalidStatusCode(details.statusCode) || + !this.activeFormSubmissionRequests.has(details.requestId) + ) { + return; + } + + const modifyLoginData = this.modifyLoginCipherFormData.get(details.tabId); + if (!modifyLoginData) { + return; + } + + this.setupNotificationInitTrigger(details.tabId, details.requestId, modifyLoginData).catch( + (error) => this.logService.error(error), + ); + }; + + /** + * Sets up the initialization trigger for the add login or change password notification. This is used + * to ensure that the notification is triggered after the tab has finished loading. + * + * @param tabId - The id of the tab + * @param requestId - The request id of the web request + * @param modifyLoginData - The modified login form data + */ + private setupNotificationInitTrigger = async ( + tabId: number, + requestId: string, + modifyLoginData: ModifyLoginCipherFormData, + ) => { + const tab = await BrowserApi.getTab(tabId); + if (tab.status !== "complete") { + await this.delayNotificationInitUntilTabIsComplete(tabId, requestId, modifyLoginData); + return; + } + + await this.triggerNotificationInit(requestId, modifyLoginData, tab); + }; + + /** + * Delays the initialization of the add login or change password notification + * until the tab is complete. This is used to ensure that the notification is + * triggered after the tab has finished loading. + * + * @param tabId - The id of the tab + * @param requestId - The request id of the web request + * @param modifyLoginData - The modified login form data + */ + private delayNotificationInitUntilTabIsComplete = async ( + tabId: chrome.webRequest.ResourceRequest["tabId"], + requestId: chrome.webRequest.ResourceRequest["requestId"], + modifyLoginData: ModifyLoginCipherFormData, + ) => { + const handleWebNavigationOnCompleted = async () => { + chrome.webNavigation.onCompleted.removeListener(handleWebNavigationOnCompleted); + const tab = await BrowserApi.getTab(tabId); + await this.triggerNotificationInit(requestId, modifyLoginData, tab); + }; + chrome.webNavigation.onCompleted.addListener(handleWebNavigationOnCompleted); + }; + + /** + * Initializes the add login or change password notification based on the modified login form data + * and the tab details. This will trigger the notification to be displayed to the user. + * + * @param requestId - The details of the web response + * @param modifyLoginData - The modified login form data + * @param tab - The tab details + */ + private triggerNotificationInit = async ( + requestId: chrome.webRequest.ResourceRequest["requestId"], + modifyLoginData: ModifyLoginCipherFormData, + tab: chrome.tabs.Tab, + ) => { + if (this.shouldTriggerChangePasswordNotification(modifyLoginData)) { + // These notifications are temporarily setup as "messages" to the notification background. + // This will be structured differently in a future refactor. + await this.notificationBackground.changedPassword( + { + command: "bgChangedPassword", + data: { + url: modifyLoginData.uri, + currentPassword: modifyLoginData.password, + newPassword: modifyLoginData.newPassword, + }, + }, + { tab }, + ); + this.clearCompletedWebRequest(requestId, tab); + return; + } + + if (this.shouldTriggerAddLoginNotification(modifyLoginData)) { + await this.notificationBackground.addLogin( + { + command: "bgAddLogin", + login: { + url: modifyLoginData.uri, + username: modifyLoginData.username, + password: modifyLoginData.password || modifyLoginData.newPassword, + }, + }, + { tab }, + ); + this.clearCompletedWebRequest(requestId, tab); + } + }; + + /** + * Determines if the change password notification should be triggered. + * + * @param modifyLoginData - The modified login form data + */ + private shouldTriggerChangePasswordNotification = ( + modifyLoginData: ModifyLoginCipherFormData, + ) => { + return modifyLoginData.newPassword && !modifyLoginData.username; + }; + + /** + * Determines if the add login notification should be triggered. + * + * @param modifyLoginData - The modified login form data + */ + private shouldTriggerAddLoginNotification = (modifyLoginData: ModifyLoginCipherFormData) => { + return modifyLoginData.username && (modifyLoginData.password || modifyLoginData.newPassword); + }; + + /** + * Clears the completed web request and removes the modified login form data for the tab. + * + * @param requestId - The request id of the web request + * @param tab - The tab details + */ + private clearCompletedWebRequest = ( + requestId: chrome.webRequest.ResourceRequest["requestId"], + tab: chrome.tabs.Tab, + ) => { + this.activeFormSubmissionRequests.delete(requestId); + this.modifyLoginCipherFormData.delete(tab.id); + this.websiteOriginsWithFields.delete(tab.id); + this.setupWebRequestsListeners(); + }; + + /** + * Determines if the status code of the web response is invalid. An invalid status code is + * any status code that is not in the 200-299 range. + * + * @param statusCode - The status code of the web response + */ + private isInvalidStatusCode = (statusCode: number) => { + return statusCode < 200 || statusCode >= 300; + }; + + /** + * Determines if the host of the web request is invalid. An invalid host is any host that does not + * start with "http" or a tab id that is less than 0. + * + * @param details - The details of the web request + */ + private requestHostIsInvalid = (details: chrome.webRequest.ResourceRequest) => { + return !details.url?.startsWith("http") || details.tabId < 0; + }; + + /** + * Sets up the listeners for the extension messages and the tab events. + */ + private setupExtensionListeners() { + BrowserApi.messageListener("overlay-notifications", this.handleExtensionMessage); + chrome.tabs.onRemoved.addListener(this.handleTabRemoved); + chrome.tabs.onUpdated.addListener(this.handleTabUpdated); + } + + /** + * Handles messages that are sent to the extension background. + * + * @param message - The message from the content script + * @param sender - The sender of the message + * @param sendResponse - The response to send back to the content script + */ + private handleExtensionMessage = ( + message: OverlayNotificationsExtensionMessage, + sender: chrome.runtime.MessageSender, + sendResponse: (response?: any) => void, + ) => { + const handler: CallableFunction = 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; + }; + + /** + * Handles the removal of a tab. This is used to remove the modified login form data for the tab. + * + * @param tabId - The id of the tab that was removed + */ + private handleTabRemoved = (tabId: number) => { + this.modifyLoginCipherFormData.delete(tabId); + if (this.websiteOriginsWithFields.has(tabId)) { + this.websiteOriginsWithFields.delete(tabId); + this.setupWebRequestsListeners(); + } + }; + + /** + * Handles the update of a tab. This is used to remove the modified + * login form data for the tab when the tab is loading. + * + * @param tabId - The id of the tab that was updated + * @param changeInfo - The change info of the tab + */ + private handleTabUpdated = (tabId: number, changeInfo: chrome.tabs.TabChangeInfo) => { + if (changeInfo.status === "loading" && this.websiteOriginsWithFields.has(tabId)) { + this.websiteOriginsWithFields.delete(tabId); + } + }; +} diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index df4867640f4..30f19e7260a 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -1,40 +1,50 @@ import { mock, MockProxy, mockReset } from "jest-mock-extended"; -import { BehaviorSubject, of } from "rxjs"; +import { BehaviorSubject } from "rxjs"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { - SHOW_AUTOFILL_BUTTON, AutofillOverlayVisibility, + SHOW_AUTOFILL_BUTTON, } from "@bitwarden/common/autofill/constants"; -import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { AutofillSettingsServiceAbstraction as AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DefaultDomainSettingsService, 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CloudEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; -import { I18nService } from "@bitwarden/common/platform/services/i18n.service"; +import { Fido2ActiveRequestManager } from "@bitwarden/common/platform/services/fido2/fido2-active-request-manager"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { - FakeStateProvider, FakeAccountService, + FakeStateProvider, mockAccountServiceWith, } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; -import { CipherType } from "@bitwarden/common/vault/enums"; -import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; +import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; +import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view"; import { BrowserApi } from "../../platform/browser/browser-api"; -import { DefaultBrowserStateService } from "../../platform/services/default-browser-state.service"; import { BrowserPlatformUtilsService } from "../../platform/services/platform-utils/browser-platform-utils.service"; +import { + AutofillOverlayElement, + AutofillOverlayPort, + MAX_SUB_FRAME_DEPTH, + RedirectFocusDirection, +} from "../enums/autofill-overlay.enum"; import { AutofillService } from "../services/abstractions/autofill.service"; import { createAutofillPageDetailsMock, @@ -43,447 +53,2596 @@ import { createPageDetailMock, createPortSpyMock, } from "../spec/autofill-mocks"; -import { flushPromises, sendMockExtensionMessage, sendPortMessage } from "../spec/testing-utils"; import { - AutofillOverlayElement, - AutofillOverlayPort, - RedirectFocusDirection, -} from "../utils/autofill-overlay.enum"; + flushPromises, + sendMockExtensionMessage, + sendPortMessage, + triggerPortOnConnectEvent, + triggerPortOnDisconnectEvent, + triggerPortOnMessageEvent, + triggerWebNavigationOnCommittedEvent, +} from "../spec/testing-utils"; -import OverlayBackground from "./overlay.background"; +import { + FocusedFieldData, + PageDetailsForTab, + SubFrameOffsetData, + SubFrameOffsetsForTab, +} from "./abstractions/overlay.background"; +import { OverlayBackground } from "./overlay.background"; describe("OverlayBackground", () => { const mockUserId = Utils.newGuid() as UserId; - const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); - const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService); + const sendResponse = jest.fn(); + let accountService: FakeAccountService; + let fakeStateProvider: FakeStateProvider; + let showFaviconsMock$: BehaviorSubject; + let neverDomainsMock$: BehaviorSubject; let domainSettingsService: DomainSettingsService; - let buttonPortSpy: chrome.runtime.Port; - let listPortSpy: chrome.runtime.Port; - let overlayBackground: OverlayBackground; - const cipherService = mock(); - const autofillService = mock(); + let logService: MockProxy; + let cipherService: MockProxy; + let autofillService: MockProxy; let activeAccountStatusMock$: BehaviorSubject; let authService: MockProxy; + let environmentMock$: BehaviorSubject; + let environmentService: MockProxy; + let inlineMenuVisibilityMock$: BehaviorSubject; + let autofillSettingsService: MockProxy; + let i18nService: MockProxy; + let platformUtilsService: MockProxy; + let enablePasskeysMock$: BehaviorSubject; + let vaultSettingsServiceMock: MockProxy; + let fido2ActiveRequestManager: Fido2ActiveRequestManager; + let selectedThemeMock$: BehaviorSubject; + let themeStateService: MockProxy; + let overlayBackground: OverlayBackground; + let portKeyForTabSpy: Record; + let pageDetailsForTabSpy: PageDetailsForTab; + let subFrameOffsetsSpy: SubFrameOffsetsForTab; + let getFrameDetailsSpy: jest.SpyInstance; + let tabsSendMessageSpy: jest.SpyInstance; + let tabSendMessageDataSpy: jest.SpyInstance; + let sendMessageSpy: jest.SpyInstance; + let getTabFromCurrentWindowIdSpy: jest.SpyInstance; + let getTabSpy: jest.SpyInstance; + let openUnlockPopoutSpy: jest.SpyInstance; + let buttonPortSpy: chrome.runtime.Port; + let buttonMessageConnectorSpy: chrome.runtime.Port; + let listPortSpy: chrome.runtime.Port; + let listMessageConnectorSpy: chrome.runtime.Port; - const environmentService = mock(); - environmentService.environment$ = new BehaviorSubject( - new CloudEnvironment({ - key: Region.US, - domain: "bitwarden.com", - urls: { icons: "https://icons.bitwarden.com/" }, - }), - ); - const stateService = mock(); - const autofillSettingsService = mock(); - const i18nService = mock(); - const platformUtilsService = mock(); - const themeStateService = mock(); - const initOverlayElementPorts = async (options = { initList: true, initButton: true }) => { + let getFrameCounter: number = 2; + async function initOverlayElementPorts(options = { initList: true, initButton: true }) { const { initList, initButton } = options; if (initButton) { - await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.Button)); - buttonPortSpy = overlayBackground["overlayButtonPort"]; + triggerPortOnConnectEvent(createPortSpyMock(AutofillOverlayPort.Button)); + buttonPortSpy = overlayBackground["inlineMenuButtonPort"]; + + buttonMessageConnectorSpy = createPortSpyMock(AutofillOverlayPort.ButtonMessageConnector); + triggerPortOnConnectEvent(buttonMessageConnectorSpy); } if (initList) { - await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.List)); - listPortSpy = overlayBackground["overlayListPort"]; + triggerPortOnConnectEvent(createPortSpyMock(AutofillOverlayPort.List)); + listPortSpy = overlayBackground["inlineMenuListPort"]; + + listMessageConnectorSpy = createPortSpyMock(AutofillOverlayPort.ListMessageConnector); + triggerPortOnConnectEvent(listMessageConnectorSpy); } return { buttonPortSpy, listPortSpy }; - }; + } beforeEach(() => { + 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(); activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked); authService = mock(); authService.activeAccountStatus$ = activeAccountStatusMock$; + environmentMock$ = new BehaviorSubject( + new CloudEnvironment({ + key: Region.US, + domain: "bitwarden.com", + urls: { icons: "https://icons.bitwarden.com/" }, + }), + ); + environmentService = mock(); + environmentService.environment$ = environmentMock$; + inlineMenuVisibilityMock$ = new BehaviorSubject(AutofillOverlayVisibility.OnFieldFocus); + autofillSettingsService = mock(); + autofillSettingsService.inlineMenuVisibility$ = inlineMenuVisibilityMock$; + i18nService = mock(); + platformUtilsService = mock(); + enablePasskeysMock$ = new BehaviorSubject(true); + vaultSettingsServiceMock = mock(); + vaultSettingsServiceMock.enablePasskeys$ = enablePasskeysMock$; + fido2ActiveRequestManager = new Fido2ActiveRequestManager(); + selectedThemeMock$ = new BehaviorSubject(ThemeType.Light); + themeStateService = mock(); + themeStateService.selectedTheme$ = selectedThemeMock$; overlayBackground = new OverlayBackground( + logService, cipherService, autofillService, authService, environmentService, domainSettingsService, - stateService, autofillSettingsService, i18nService, platformUtilsService, + vaultSettingsServiceMock, + fido2ActiveRequestManager, themeStateService, ); - - jest - .spyOn(overlayBackground as any, "getOverlayVisibility") - .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); - - themeStateService.selectedTheme$ = of(ThemeType.Light); - domainSettingsService.showFavicons$ = of(true); + portKeyForTabSpy = overlayBackground["portKeyForTab"]; + pageDetailsForTabSpy = overlayBackground["pageDetailsForTab"]; + subFrameOffsetsSpy = overlayBackground["subFrameOffsetsForTab"]; + getFrameDetailsSpy = jest.spyOn(BrowserApi, "getFrameDetails"); + getFrameDetailsSpy.mockImplementation((_details: chrome.webNavigation.GetFrameDetails) => { + getFrameCounter--; + return mock({ + parentFrameId: getFrameCounter, + }); + }); + tabsSendMessageSpy = jest + .spyOn(BrowserApi, "tabSendMessage") + .mockImplementation(() => Promise.resolve()); + tabSendMessageDataSpy = jest + .spyOn(BrowserApi, "tabSendMessageData") + .mockImplementation(() => Promise.resolve()); + sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage"); + getTabFromCurrentWindowIdSpy = jest.spyOn(BrowserApi, "getTabFromCurrentWindowId"); + getTabSpy = jest.spyOn(BrowserApi, "getTab"); + openUnlockPopoutSpy = jest.spyOn(overlayBackground as any, "openUnlockPopout"); void overlayBackground.init(); }); afterEach(() => { + getFrameCounter = 2; jest.clearAllMocks(); + jest.useRealTimers(); mockReset(cipherService); }); - describe("removePageDetails", () => { - it("removes the page details for a specific tab from the pageDetailsForTab object", () => { + describe("storing pageDetails", () => { + const tabId = 1; + + beforeEach(() => { + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ tab: createChromeTabMock({ id: tabId }), frameId: 0 }), + ); + }); + + it("stores the page details for the tab", () => { + expect(pageDetailsForTabSpy[tabId]).toBeDefined(); + }); + + describe("building sub frame offsets", () => { + beforeEach(() => { + tabsSendMessageSpy.mockResolvedValue( + mock({ + left: getFrameCounter, + top: getFrameCounter, + url: "url", + }), + ); + }); + + it("triggers a destruction of the inline menu listeners if the max frame depth is exceeded ", async () => { + getFrameCounter = MAX_SUB_FRAME_DEPTH + 1; + const tab = createChromeTabMock({ id: tabId }); + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ + tab, + frameId: 1, + }), + ); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + tab, + { command: "destroyAutofillInlineMenuListeners" }, + { frameId: 1 }, + ); + }); + + it("builds the offset values for a sub frame within the tab", async () => { + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ + tab: createChromeTabMock({ id: tabId }), + frameId: 1, + }), + ); + await flushPromises(); + + expect(subFrameOffsetsSpy[tabId]).toStrictEqual( + new Map([[1, { left: 4, top: 4, url: "url", parentFrameIds: [0, 1] }]]), + ); + expect(pageDetailsForTabSpy[tabId].size).toBe(2); + }); + + it("skips building offset values for a previously calculated sub frame", async () => { + getFrameCounter = 0; + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ + tab: createChromeTabMock({ id: tabId }), + frameId: 1, + }), + ); + await flushPromises(); + + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ + tab: createChromeTabMock({ id: tabId }), + frameId: 1, + }), + ); + await flushPromises(); + + expect(getFrameDetailsSpy).toHaveBeenCalledTimes(1); + expect(subFrameOffsetsSpy[tabId]).toStrictEqual( + new Map([[1, { left: 0, top: 0, url: "url", parentFrameIds: [0] }]]), + ); + }); + + it("will attempt to build the sub frame offsets by posting window messages if a set of offsets is not returned", async () => { + const tab = createChromeTabMock({ id: tabId }); + const frameId = 1; + tabsSendMessageSpy.mockResolvedValue(null); + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ tab, frameId }), + ); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + tab, + { + command: "getSubFrameOffsetsFromWindowMessage", + subFrameId: frameId, + }, + { frameId }, + ); + expect(subFrameOffsetsSpy[tabId]).toStrictEqual(new Map([[frameId, null]])); + }); + + it("updates sub frame data that has been calculated using window messages", async () => { + const tab = createChromeTabMock({ id: tabId }); + const frameId = 1; + const subFrameData = mock({ frameId, left: 10, top: 10, url: "url" }); + tabsSendMessageSpy.mockResolvedValueOnce(null); + subFrameOffsetsSpy[tabId] = new Map([[frameId, null]]); + + sendMockExtensionMessage( + { command: "updateSubFrameData", subFrameData }, + mock({ tab, frameId }), + ); + await flushPromises(); + + expect(subFrameOffsetsSpy[tabId]).toStrictEqual(new Map([[frameId, subFrameData]])); + }); + }); + }); + + describe("removing pageDetails", () => { + it("removes the page details and port key for a specific tab from the pageDetailsForTab object", async () => { + await initOverlayElementPorts(); const tabId = 1; - const frameId = 2; - overlayBackground["pageDetailsForTab"][tabId] = new Map([[frameId, createPageDetailMock()]]); + portKeyForTabSpy[tabId] = "portKey"; + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: createAutofillPageDetailsMock() }, + mock({ tab: createChromeTabMock({ id: tabId }), frameId: 1 }), + ); + overlayBackground.removePageDetails(tabId); - expect(overlayBackground["pageDetailsForTab"][tabId]).toBeUndefined(); + expect(pageDetailsForTabSpy[tabId]).toBeUndefined(); + expect(portKeyForTabSpy[tabId]).toBeUndefined(); }); }); - describe("init", () => { - it("sets up the extension message listeners, get the overlay's visibility settings, and get the user's auth status", async () => { - overlayBackground["setupExtensionMessageListeners"] = jest.fn(); - overlayBackground["getOverlayVisibility"] = jest.fn(); - overlayBackground["getAuthStatus"] = jest.fn(); + describe("re-positioning the inline menu within sub frames", () => { + const tabId = 1; + const topFrameId = 0; + const middleFrameId = 10; + const middleAdjacentFrameId = 11; + const bottomFrameId = 20; + let tab: chrome.tabs.Tab; + let sender: MockProxy; - await overlayBackground.init(); + async function flushOverlayRepositionPromises() { + await flushPromises(); + jest.advanceTimersByTime(1150); + await flushPromises(); + } - expect(overlayBackground["setupExtensionMessageListeners"]).toHaveBeenCalled(); - expect(overlayBackground["getOverlayVisibility"]).toHaveBeenCalled(); - expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); + beforeEach(() => { + jest.useFakeTimers(); + tab = createChromeTabMock({ id: tabId }); + sender = mock({ tab, frameId: middleFrameId }); + overlayBackground["focusedFieldData"] = mock({ + tabId, + frameId: bottomFrameId, + }); + subFrameOffsetsSpy[tabId] = new Map([ + [topFrameId, { left: 1, top: 1, url: "https://top-frame.com", parentFrameIds: [] }], + [ + middleFrameId, + { left: 2, top: 2, url: "https://middle-frame.com", parentFrameIds: [topFrameId] }, + ], + [ + middleAdjacentFrameId, + { + left: 3, + top: 3, + url: "https://middle-adjacent-frame.com", + parentFrameIds: [topFrameId], + }, + ], + [ + bottomFrameId, + { + left: 4, + top: 4, + url: "https://bottom-frame.com", + parentFrameIds: [topFrameId, middleFrameId], + }, + ], + ]); + tabsSendMessageSpy.mockResolvedValue( + mock({ + left: getFrameCounter, + top: getFrameCounter, + url: "url", + }), + ); + }); + + describe("triggerAutofillOverlayReposition", () => { + describe("checkShouldRepositionInlineMenu", () => { + let focusedFieldData: FocusedFieldData; + let repositionInlineMenuSpy: jest.SpyInstance; + + beforeEach(() => { + focusedFieldData = createFocusedFieldDataMock({ tabId }); + repositionInlineMenuSpy = jest.spyOn(overlayBackground as any, "repositionInlineMenu"); + }); + + describe("blocking a reposition of the overlay", () => { + it("blocks repositioning when the focused field data is not set", async () => { + overlayBackground["focusedFieldData"] = undefined; + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushOverlayRepositionPromises(); + + expect(repositionInlineMenuSpy).not.toHaveBeenCalled(); + }); + + it("blocks repositioning when the sender is from a different tab than the focused field", async () => { + const otherSender = mock({ frameId: 1, tab: { id: 2 } }); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData }, + otherSender, + ); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushOverlayRepositionPromises(); + + expect(repositionInlineMenuSpy).not.toHaveBeenCalled(); + }); + + it("blocks repositioning when the sender frame is not a parent frame of the focused field", async () => { + focusedFieldData = createFocusedFieldDataMock({ tabId }); + const otherFrameSender = mock({ + tab, + frameId: middleAdjacentFrameId, + }); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData }, + otherFrameSender, + ); + sender.frameId = bottomFrameId; + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushOverlayRepositionPromises(); + + expect(repositionInlineMenuSpy).not.toHaveBeenCalled(); + }); + }); + + describe("allowing a reposition of the overlay", () => { + it("allows repositioning when the sender frame is for the focused field and the inline menu is visible, ", async () => { + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData }, + sender, + ); + tabsSendMessageSpy.mockImplementation((_tab, message) => { + if (message.command === "checkIsAutofillInlineMenuButtonVisible") { + return Promise.resolve(true); + } + }); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushOverlayRepositionPromises(); + + expect(repositionInlineMenuSpy).toHaveBeenCalled(); + }); + }); + }); + + describe("repositionInlineMenu", () => { + beforeEach(() => { + overlayBackground["isFieldCurrentlyFocused"] = true; + }); + + it("closes the inline menu if the field is not focused", async () => { + overlayBackground["isFieldCurrentlyFocused"] = false; + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushOverlayRepositionPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + tab, + { command: "closeAutofillInlineMenu" }, + { frameId: 0 }, + ); + }); + + it("closes the inline menu if the focused field is not within the viewport", async () => { + tabsSendMessageSpy.mockImplementation((_tab, message) => { + if (message.command === "checkIsMostRecentlyFocusedFieldWithinViewport") { + return Promise.resolve(false); + } + }); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushOverlayRepositionPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + tab, + { command: "closeAutofillInlineMenu" }, + { frameId: 0 }, + ); + }); + + it("rebuilds the sub frame offsets when the focused field's frame id indicates that it is within a sub frame", async () => { + const focusedFieldData = createFocusedFieldDataMock({ tabId, frameId: middleFrameId }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushOverlayRepositionPromises(); + + expect(getFrameDetailsSpy).toHaveBeenCalledWith({ tabId, frameId: middleFrameId }); + }); + + describe("updating the inline menu position", () => { + let sender: chrome.runtime.MessageSender; + + async function flushUpdateInlineMenuPromises() { + await flushOverlayRepositionPromises(); + await flushPromises(); + jest.advanceTimersByTime(250); + await flushPromises(); + } + + beforeEach(async () => { + sender = mock({ tab, frameId: middleFrameId }); + jest.useFakeTimers(); + await initOverlayElementPorts(); + }); + + it("skips updating the position of either inline menu element if a field is not currently focused", async () => { + sendMockExtensionMessage( + { + command: "updateIsFieldCurrentlyFocused", + isFieldCurrentlyFocused: false, + }, + mock({ frameId: 20 }), + ); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushUpdateInlineMenuPromises(); + + expect(tabsSendMessageSpy).not.toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.Button, + }, + { frameId: 0 }, + ); + expect(tabsSendMessageSpy).not.toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.List, + }, + { frameId: 0 }, + ); + }); + + it("sets the inline menu invisible and updates its position", async () => { + overlayBackground["checkIsInlineMenuButtonVisible"] = jest + .fn() + .mockResolvedValue(false); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushUpdateInlineMenuPromises(); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "toggleAutofillInlineMenuHidden", + styles: { display: "none" }, + }); + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.Button, + }, + { frameId: 0 }, + ); + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.List, + }, + { frameId: 0 }, + ); + }); + + it("skips updating the inline menu list if the user has the inline menu set to open on button click", async () => { + inlineMenuVisibilityMock$.next(AutofillOverlayVisibility.OnButtonClick); + tabsSendMessageSpy.mockImplementation((_tab, message, _options) => { + if (message.command === "checkMostRecentlyFocusedFieldHasValue") { + return Promise.resolve(true); + } + + return Promise.resolve({}); + }); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushUpdateInlineMenuPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.Button, + }, + { frameId: 0 }, + ); + expect(tabsSendMessageSpy).not.toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.List, + }, + { frameId: 0 }, + ); + }); + + it("skips updating the inline menu list if the focused field has a value and the user status is not unlocked", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Locked); + tabsSendMessageSpy.mockImplementation((_tab, message, _options) => { + if (message.command === "checkMostRecentlyFocusedFieldHasValue") { + return Promise.resolve(true); + } + + return Promise.resolve({}); + }); + + sendMockExtensionMessage({ command: "triggerAutofillOverlayReposition" }, sender); + await flushUpdateInlineMenuPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.Button, + }, + { frameId: 0 }, + ); + expect(tabsSendMessageSpy).not.toHaveBeenCalledWith( + sender.tab, + { + command: "appendAutofillInlineMenuToDom", + overlayElement: AutofillOverlayElement.List, + }, + { frameId: 0 }, + ); + }); + }); + }); + + describe("triggerSubFrameFocusInRebuild", () => { + it("triggers a rebuild of the sub frame and updates the inline menu position", async () => { + const rebuildSubFrameOffsetsSpy = jest.spyOn( + overlayBackground as any, + "rebuildSubFrameOffsets", + ); + const repositionInlineMenuSpy = jest.spyOn( + overlayBackground as any, + "repositionInlineMenu", + ); + + sendMockExtensionMessage({ command: "triggerSubFrameFocusInRebuild" }, sender); + await flushOverlayRepositionPromises(); + + expect(rebuildSubFrameOffsetsSpy).toHaveBeenCalled(); + expect(repositionInlineMenuSpy).toHaveBeenCalled(); + }); + }); + + describe("toggleInlineMenuHidden", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + }); + + it("skips adjusting the hidden status of the inline menu if the sender tab does not contain the focused field", async () => { + const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + const otherSender = mock({ tab: { id: 2 } }); + + await overlayBackground["toggleInlineMenuHidden"]( + { isInlineMenuHidden: true }, + otherSender, + ); + + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "toggleAutofillInlineMenuHidden", + styles: { display: "none" }, + }); + }); + }); }); }); - describe("updateOverlayCiphers", () => { + 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 }, + login: { username: "username-1", password: "password", uri: url }, }); - const cipher2 = mock({ + const cardCipher = mock({ id: "id-2", - localData: { lastUsedDate: 111 }, + localData: { lastUsedDate: 222 }, name: "name-2", + type: CipherType.Card, + card: { subTitle: "subtitle-2" }, + }); + const loginCipher2 = mock({ + id: "id-3", + localData: { lastUsedDate: 222 }, + name: "name-3", type: CipherType.Login, - login: { username: "username-2", uri: url }, + login: { username: "username-3", uri: url }, + }); + const identityCipher = mock({ + id: "id-4", + localData: { lastUsedDate: 222 }, + name: "name-4", + type: CipherType.Identity, + identity: { + username: "username", + firstName: "Test", + lastName: "User", + email: "email@example.com", + }, + }); + const passkeyCipher = mock({ + id: "id-5", + localData: { lastUsedDate: 222 }, + name: "name-5", + type: CipherType.Login, + login: { + username: "username-5", + password: "password", + uri: url, + fido2Credentials: [ + mock({ + credentialId: "credential-id", + rpName: "credential-name", + userName: "credential-username", + rpId: "jest-testing-website.com", + }), + ], + }, }); - beforeEach(() => { + beforeEach(async () => { activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + await initOverlayElementPorts(); }); - it("ignores updating the overlay ciphers if the user's auth status is not unlocked", async () => { + it("skips updating the overlay ciphers if the user's auth status is not unlocked", async () => { activeAccountStatusMock$.next(AuthenticationStatus.Locked); - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId"); - jest.spyOn(cipherService, "getAllDecryptedForUrl"); await overlayBackground.updateOverlayCiphers(); - expect(BrowserApi.getTabFromCurrentWindowId).not.toHaveBeenCalled(); + expect(getTabFromCurrentWindowIdSpy).not.toHaveBeenCalled(); expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled(); }); - it("ignores updating the overlay ciphers if the tab is undefined", async () => { - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(undefined); - jest.spyOn(cipherService, "getAllDecryptedForUrl"); + it("skips updating the inline menu ciphers if the current tab url has non-http protocol", async () => { + const nonHttpTab = createChromeTabMock({ url: "chrome-extension://id/route" }); + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(nonHttpTab); + + await overlayBackground.updateOverlayCiphers(); + + expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled(); + }); + + it("closes the inline menu on the focused field's tab if the user's auth status is not unlocked", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Locked); + const previousTab = mock({ id: 1 }); + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: 1 }); + getTabSpy.mockResolvedValueOnce(previousTab); + + await overlayBackground.updateOverlayCiphers(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + previousTab, + { command: "closeAutofillInlineMenu", overlayElement: undefined }, + { frameId: 0 }, + ); + }); + + it("closes the inline menu on the focused field's tab if current tab is different", async () => { + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); + cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, cardCipher]); + cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); + const previousTab = mock({ id: 15 }); + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ tabId: 15 }); + getTabSpy.mockResolvedValueOnce(previousTab); + + await overlayBackground.updateOverlayCiphers(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + previousTab, + { command: "closeAutofillInlineMenu", overlayElement: undefined }, + { frameId: 0 }, + ); + }); + + 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([loginCipher1, cardCipher]); + cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); await overlayBackground.updateOverlayCiphers(); expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); - expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled(); + expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, [ + CipherType.Card, + CipherType.Identity, + ]); + expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled(); + expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual( + new Map([ + ["inline-menu-cipher-0", cardCipher], + ["inline-menu-cipher-1", loginCipher1], + ]), + ); }); - it("queries all ciphers for the given url, sort them by last used, and format them for usage in the overlay", async () => { - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(tab); - cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); + it("queries only login ciphers when not updating all cipher types", async () => { + overlayBackground["cardAndIdentityCiphers"] = new Set([]); + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); + cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher2, loginCipher1]); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); - jest.spyOn(overlayBackground as any, "getOverlayCipherData"); - await overlayBackground.updateOverlayCiphers(); + await overlayBackground.updateOverlayCiphers(false); expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url); - expect(overlayBackground["cipherService"].sortCiphersByLastUsedThenName).toHaveBeenCalled(); - expect(overlayBackground["overlayLoginCiphers"]).toStrictEqual( + expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled(); + expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual( new Map([ - ["overlay-cipher-0", cipher2], - ["overlay-cipher-1", cipher1], + ["inline-menu-cipher-0", loginCipher1], + ["inline-menu-cipher-1", loginCipher2], ]), ); - expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled(); }); - it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateIsOverlayCiphersPopulated` message to the tab indicating that the list of ciphers is populated", async () => { - overlayBackground["overlayListPort"] = mock(); - cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); + 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([loginCipher1, cardCipher]); cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(tab); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); + + await overlayBackground.updateOverlayCiphers(false); + + expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); + expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, [ + CipherType.Card, + CipherType.Identity, + ]); + expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled(); + expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual( + new Map([ + ["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([loginCipher1]); + cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); await overlayBackground.updateOverlayCiphers(); - expect(overlayBackground["overlayListPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayListCiphers", + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuListCiphers", + showInlineMenuAccountCreation: false, + showPasskeysLabels: false, ciphers: [ { - card: null, - favorite: cipher2.favorite, + accountCreationFieldType: undefined, + 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, }, - id: "overlay-cipher-0", - login: { - username: "username-2", - }, - name: "name-2", - reprompt: cipher2.reprompt, - type: 1, - }, - { - card: null, - favorite: cipher1.favorite, - icon: { - fallbackImage: "images/bwi-globe.png", - icon: "bwi-globe", - image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", - imageEnabled: true, - }, - id: "overlay-cipher-1", + id: "inline-menu-cipher-0", login: { username: "username-1", + passkey: null, }, name: "name-1", - reprompt: cipher1.reprompt, - type: 1, + reprompt: loginCipher1.reprompt, + type: CipherType.Login, }, ], }); - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - tab, - "updateIsOverlayCiphersPopulated", - { isOverlayCiphersPopulated: true }, + }); + + it("updates the inline menu list with card ciphers", async () => { + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ + tabId: tab.id, + filledByCipherType: CipherType.Card, + }); + cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, cardCipher]); + cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); + + await overlayBackground.updateOverlayCiphers(); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuListCiphers", + showInlineMenuAccountCreation: false, + showPasskeysLabels: false, + ciphers: [ + { + accountCreationFieldType: undefined, + favorite: cardCipher.favorite, + icon: { + fallbackImage: "", + icon: "bwi-credit-card", + image: undefined, + imageEnabled: true, + }, + id: "inline-menu-cipher-0", + card: cardCipher.card.subTitle, + name: cardCipher.name, + reprompt: cardCipher.reprompt, + type: CipherType.Card, + }, + ], + }); + }); + + describe("updating ciphers for an account creation inline menu", () => { + it("updates the ciphers with a list of identity ciphers that contain a username", async () => { + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ + tabId: tab.id, + accountCreationFieldType: "text", + showInlineMenuAccountCreation: true, + }); + cipherService.getAllDecryptedForUrl.mockResolvedValue([identityCipher, cardCipher]); + cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); + + await overlayBackground.updateOverlayCiphers(); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuListCiphers", + showInlineMenuAccountCreation: true, + showPasskeysLabels: false, + ciphers: [ + { + accountCreationFieldType: "text", + favorite: identityCipher.favorite, + icon: { + fallbackImage: "", + icon: "bwi-id-card", + image: undefined, + imageEnabled: true, + }, + id: "inline-menu-cipher-1", + name: identityCipher.name, + reprompt: identityCipher.reprompt, + type: CipherType.Identity, + identity: { + fullName: `${identityCipher.identity.firstName} ${identityCipher.identity.lastName}`, + username: identityCipher.identity.username, + }, + }, + ], + }); + }); + + it("appends any found login ciphers to the list of identity ciphers", async () => { + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ + tabId: tab.id, + accountCreationFieldType: "text", + showInlineMenuAccountCreation: true, + }); + cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, identityCipher]); + cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); + + await overlayBackground.updateOverlayCiphers(); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuListCiphers", + showInlineMenuAccountCreation: true, + showPasskeysLabels: false, + ciphers: [ + { + accountCreationFieldType: "text", + favorite: identityCipher.favorite, + icon: { + fallbackImage: "", + icon: "bwi-id-card", + image: undefined, + imageEnabled: true, + }, + id: "inline-menu-cipher-0", + name: identityCipher.name, + reprompt: identityCipher.reprompt, + type: CipherType.Identity, + identity: { + fullName: `${identityCipher.identity.firstName} ${identityCipher.identity.lastName}`, + username: identityCipher.identity.username, + }, + }, + { + accountCreationFieldType: "text", + 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, + }, + id: "inline-menu-cipher-1", + login: { + username: loginCipher1.login.username, + passkey: null, + }, + name: loginCipher1.name, + reprompt: loginCipher1.reprompt, + type: CipherType.Login, + }, + ], + }); + }); + + it("skips any identity ciphers that do not contain a username or an email address", async () => { + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ + tabId: tab.id, + accountCreationFieldType: "email", + showInlineMenuAccountCreation: true, + }); + const identityCipherWithoutUsername = mock({ + id: "id-5", + localData: { lastUsedDate: 222 }, + name: "name-5", + type: CipherType.Identity, + identity: { + username: "", + email: "", + }, + }); + cipherService.getAllDecryptedForUrl.mockResolvedValue([ + identityCipher, + identityCipherWithoutUsername, + ]); + cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); + + await overlayBackground.updateOverlayCiphers(); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuListCiphers", + showInlineMenuAccountCreation: true, + showPasskeysLabels: false, + ciphers: [ + { + accountCreationFieldType: "email", + favorite: identityCipher.favorite, + icon: { + fallbackImage: "", + icon: "bwi-id-card", + image: undefined, + imageEnabled: true, + }, + id: "inline-menu-cipher-1", + name: identityCipher.name, + reprompt: identityCipher.reprompt, + type: CipherType.Identity, + identity: { + fullName: `${identityCipher.identity.firstName} ${identityCipher.identity.lastName}`, + username: identityCipher.identity.email, + }, + }, + ], + }); + }); + + it("does not add the identity ciphers if the field is for a password field", async () => { + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ + tabId: tab.id, + accountCreationFieldType: "password", + showInlineMenuAccountCreation: true, + }); + cipherService.getAllDecryptedForUrl.mockResolvedValue([identityCipher]); + cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); + + await overlayBackground.updateOverlayCiphers(); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuListCiphers", + showInlineMenuAccountCreation: true, + showPasskeysLabels: false, + ciphers: [], + }); + }); + }); + + it("adds available passkey ciphers to the inline menu", async () => { + void fido2ActiveRequestManager.newActiveRequest( + tab.id, + passkeyCipher.login.fido2Credentials, + new AbortController(), ); - }); - }); + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ + tabId: tab.id, + filledByCipherType: CipherType.Login, + showPasskeys: true, + }); + cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, passkeyCipher]); + cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); - describe("getOverlayCipherData", () => { - const url = "https://jest-testing-website.com"; - const cipher1 = mock({ - id: "id-1", - localData: { lastUsedDate: 222 }, - name: "name-1", - type: CipherType.Login, - login: { username: "username-1", uri: url }, - }); - const cipher2 = mock({ - id: "id-2", - localData: { lastUsedDate: 111 }, - name: "name-2", - type: CipherType.Login, - login: { username: "username-2", uri: url }, - }); - const cipher3 = mock({ - id: "id-3", - localData: { lastUsedDate: 333 }, - name: "name-3", - type: CipherType.Card, - card: { subTitle: "Visa, *6789" }, - }); - const cipher4 = mock({ - id: "id-4", - localData: { lastUsedDate: 444 }, - name: "name-4", - type: CipherType.Card, - card: { subTitle: "Mastercard, *1234" }, - }); + await overlayBackground.updateOverlayCiphers(); - it("formats and returns the cipher data", async () => { - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-0", cipher2], - ["overlay-cipher-1", cipher1], - ["overlay-cipher-2", cipher3], - ["overlay-cipher-3", cipher4], - ]); - - const overlayCipherData = await overlayBackground["getOverlayCipherData"](); - - expect(overlayCipherData).toStrictEqual([ - { - card: null, - favorite: cipher2.favorite, - icon: { - fallbackImage: "images/bwi-globe.png", - icon: "bwi-globe", - image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", - imageEnabled: true, + 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: "overlay-cipher-0", - login: { - username: "username-2", + { + 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, + }, }, - name: "name-2", - reprompt: cipher2.reprompt, - type: 1, - }, - { - card: null, - favorite: cipher1.favorite, - icon: { - fallbackImage: "images/bwi-globe.png", - icon: "bwi-globe", - image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", - imageEnabled: true, + { + 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, + }, }, - id: "overlay-cipher-1", - login: { - username: "username-1", - }, - name: "name-1", - reprompt: cipher1.reprompt, - type: 1, - }, - { - card: "Visa, *6789", - favorite: cipher3.favorite, - icon: { - fallbackImage: "", - icon: "bwi-credit-card", - image: undefined, - imageEnabled: true, - }, - id: "overlay-cipher-2", - login: null, - name: "name-3", - reprompt: cipher3.reprompt, - type: 3, - }, - { - card: "Mastercard, *1234", - favorite: cipher4.favorite, - icon: { - fallbackImage: "", - icon: "bwi-credit-card", - image: undefined, - imageEnabled: true, - }, - id: "overlay-cipher-3", - login: null, - name: "name-4", - reprompt: cipher4.reprompt, - type: 3, - }, - ]); - }); - }); - - describe("getAuthStatus", () => { - it("will update the user's auth status but will not update the overlay ciphers", async () => { - const authStatus = AuthenticationStatus.Unlocked; - overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; - jest.spyOn(overlayBackground["authService"], "getAuthStatus").mockResolvedValue(authStatus); - jest.spyOn(overlayBackground as any, "updateOverlayButtonAuthStatus").mockImplementation(); - jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation(); - - const status = await overlayBackground["getAuthStatus"](); - - expect(overlayBackground["authService"].getAuthStatus).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayButtonAuthStatus"]).not.toHaveBeenCalled(); - expect(overlayBackground["updateOverlayCiphers"]).not.toHaveBeenCalled(); - expect(overlayBackground["userAuthStatus"]).toBe(authStatus); - expect(status).toBe(authStatus); + ], + showInlineMenuAccountCreation: false, + showPasskeysLabels: true, + }); }); - it("will update the user's auth status and update the overlay ciphers if the status has been modified", async () => { - const authStatus = AuthenticationStatus.Unlocked; - overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; - jest.spyOn(overlayBackground["authService"], "getAuthStatus").mockResolvedValue(authStatus); - jest.spyOn(overlayBackground as any, "updateOverlayButtonAuthStatus").mockImplementation(); - jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation(); + it("does not add a passkey to the inline menu when its rpId is part of the neverDomains exclusion list", async () => { + void fido2ActiveRequestManager.newActiveRequest( + tab.id, + passkeyCipher.login.fido2Credentials, + new AbortController(), + ); + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ + tabId: tab.id, + filledByCipherType: CipherType.Login, + showPasskeys: true, + }); + cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, passkeyCipher]); + cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); + neverDomainsMock$.next({ "jest-testing-website.com": null }); - await overlayBackground["getAuthStatus"](); + await overlayBackground.updateOverlayCiphers(); - expect(overlayBackground["authService"].getAuthStatus).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayButtonAuthStatus"]).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayCiphers"]).toHaveBeenCalled(); - expect(overlayBackground["userAuthStatus"]).toBe(authStatus); + 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("updateOverlayButtonAuthStatus", () => { - it("will send a message to the button port with the user's auth status", () => { - overlayBackground["overlayButtonPort"] = mock(); - jest.spyOn(overlayBackground["overlayButtonPort"], "postMessage"); + it("does not add passkeys to the inline menu if the passkey setting is disabled", async () => { + enablePasskeysMock$.next(false); + void fido2ActiveRequestManager.newActiveRequest( + tab.id, + passkeyCipher.login.fido2Credentials, + new AbortController(), + ); + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ + tabId: tab.id, + filledByCipherType: CipherType.Login, + showPasskeys: true, + }); + cipherService.getAllDecryptedForUrl.mockResolvedValue([loginCipher1, passkeyCipher]); + cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(tab); - overlayBackground["updateOverlayButtonAuthStatus"](); + await overlayBackground.updateOverlayCiphers(); - expect(overlayBackground["overlayButtonPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayButtonAuthStatus", - authStatus: overlayBackground["userAuthStatus"], + 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("getTranslations", () => { - it("will query the overlay page translations if they have not been queried", () => { - overlayBackground["overlayPageTranslations"] = undefined; - jest.spyOn(overlayBackground as any, "getTranslations"); - jest.spyOn(overlayBackground["i18nService"], "translate").mockImplementation((key) => key); - jest.spyOn(BrowserApi, "getUILanguage").mockReturnValue("en"); + describe("extension message handlers", () => { + describe("autofillOverlayElementClosed message handler", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + }); - const translations = overlayBackground["getTranslations"](); + it("disconnects any expired ports if the sender is not from the same page as the most recently focused field", () => { + const port1 = mock(); + const port2 = mock(); + overlayBackground["expiredPorts"] = [port1, port2]; + const sender = mock({ tab: { id: 1 } }); + const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); - const translationKeys = [ - "opensInANewWindow", - "bitwardenOverlayButton", - "toggleBitwardenVaultOverlay", - "bitwardenVault", - "unlockYourAccountToViewMatchingLogins", - "unlockAccount", - "fillCredentialsFor", - "partialUsername", - "view", - "noItemsToShow", - "newItem", - "addNewVaultItem", + sendMockExtensionMessage( + { + command: "autofillOverlayElementClosed", + overlayElement: AutofillOverlayElement.Button, + }, + sender, + ); + + expect(port1.disconnect).toHaveBeenCalled(); + expect(port2.disconnect).toHaveBeenCalled(); + }); + + it("disconnects the button element port", () => { + sendMockExtensionMessage({ + command: "autofillOverlayElementClosed", + overlayElement: AutofillOverlayElement.Button, + }); + + expect(buttonPortSpy.disconnect).toHaveBeenCalled(); + }); + + it("disconnects the list element port", () => { + sendMockExtensionMessage({ + command: "autofillOverlayElementClosed", + overlayElement: AutofillOverlayElement.List, + }); + + expect(listPortSpy.disconnect).toHaveBeenCalled(); + }); + }); + + describe("autofillOverlayAddNewVaultItem message handler", () => { + let sender: chrome.runtime.MessageSender; + let openAddEditVaultItemPopoutSpy: jest.SpyInstance; + + beforeEach(() => { + jest.useFakeTimers(); + sender = mock({ + tab: { id: 1 }, + url: "https://top-frame-test.com", + frameId: 0, + }); + openAddEditVaultItemPopoutSpy = jest + .spyOn(overlayBackground as any, "openAddEditVaultItemPopout") + .mockImplementation(); + overlayBackground["currentAddNewItemData"] = { sender, addNewCipherType: CipherType.Login }; + }); + + it("will not open the add edit popout window if the message does not have a login cipher provided", () => { + sendMockExtensionMessage({ command: "autofillOverlayAddNewVaultItem" }, sender); + + expect(cipherService.setAddEditCipherInfo).not.toHaveBeenCalled(); + expect(openAddEditVaultItemPopoutSpy).not.toHaveBeenCalled(); + }); + + it("resets the currentAddNewItemData to null when a cipher view is not successfully created", async () => { + jest.spyOn(overlayBackground as any, "buildLoginCipherView").mockReturnValue(null); + + sendMockExtensionMessage( + { + command: "autofillOverlayAddNewVaultItem", + addNewCipherType: CipherType.Login, + login: { + uri: "https://tacos.com", + hostname: "", + username: "username", + password: "password", + }, + }, + sender, + ); + jest.advanceTimersByTime(100); + await flushPromises(); + + expect(overlayBackground["currentAddNewItemData"]).toBeNull(); + }); + + it("will open the add edit popout window after creating a new cipher", async () => { + sendMockExtensionMessage( + { + command: "autofillOverlayAddNewVaultItem", + addNewCipherType: CipherType.Login, + login: { + uri: "https://tacos.com", + hostname: "", + username: "username", + password: "password", + }, + }, + sender, + ); + jest.advanceTimersByTime(100); + await flushPromises(); + + expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); + expect(sendMessageSpy).toHaveBeenCalledWith("inlineAutofillMenuRefreshAddEditCipher"); + expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalled(); + }); + + it("creates a new card cipher", async () => { + overlayBackground["currentAddNewItemData"].addNewCipherType = CipherType.Card; + + sendMockExtensionMessage( + { + command: "autofillOverlayAddNewVaultItem", + addNewCipherType: CipherType.Card, + card: { + cardholderName: "cardholderName", + number: "4242424242424242", + expirationMonth: "12", + expirationYear: "2025", + expirationDate: "12/25", + cvv: "123", + }, + }, + sender, + ); + jest.advanceTimersByTime(100); + await flushPromises(); + + expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); + expect(sendMessageSpy).toHaveBeenCalledWith("inlineAutofillMenuRefreshAddEditCipher"); + expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalled(); + }); + + describe("creating a new identity cipher", () => { + beforeEach(() => { + overlayBackground["currentAddNewItemData"].addNewCipherType = CipherType.Identity; + }); + + it("populates an identity cipher view and creates it", async () => { + sendMockExtensionMessage( + { + command: "autofillOverlayAddNewVaultItem", + addNewCipherType: CipherType.Identity, + identity: { + title: "title", + firstName: "firstName", + middleName: "middleName", + lastName: "lastName", + fullName: "fullName", + address1: "address1", + address2: "address2", + address3: "address3", + city: "city", + state: "state", + postalCode: "postalCode", + country: "country", + company: "company", + phone: "phone", + email: "email", + username: "username", + }, + }, + sender, + ); + jest.advanceTimersByTime(100); + await flushPromises(); + + expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); + expect(sendMessageSpy).toHaveBeenCalledWith("inlineAutofillMenuRefreshAddEditCipher"); + expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalled(); + }); + + it("saves the first name based on the full name value", async () => { + sendMockExtensionMessage( + { + command: "autofillOverlayAddNewVaultItem", + addNewCipherType: CipherType.Identity, + identity: { + firstName: "", + lastName: "", + fullName: "fullName", + }, + }, + sender, + ); + jest.advanceTimersByTime(100); + await flushPromises(); + + expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); + }); + + it("saves the first and middle names based on the full name value", async () => { + sendMockExtensionMessage( + { + command: "autofillOverlayAddNewVaultItem", + addNewCipherType: CipherType.Identity, + identity: { + firstName: "", + lastName: "", + fullName: "firstName middleName", + }, + }, + sender, + ); + jest.advanceTimersByTime(100); + await flushPromises(); + + expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); + }); + + it("saves the first, middle, and last names based on the full name value", async () => { + sendMockExtensionMessage( + { + command: "autofillOverlayAddNewVaultItem", + addNewCipherType: CipherType.Identity, + identity: { + firstName: "", + lastName: "", + fullName: "firstName middleName lastName", + }, + }, + sender, + ); + jest.advanceTimersByTime(100); + await flushPromises(); + + expect(cipherService.setAddEditCipherInfo).toHaveBeenCalled(); + }); + }); + + describe("pulling cipher data from multiple frames of a tab", () => { + let subFrameSender: MockProxy; + let secondSubFrameSender: MockProxy; + const command = "autofillOverlayAddNewVaultItem"; + + beforeEach(() => { + subFrameSender = mock({ tab: sender.tab, frameId: 2 }); + secondSubFrameSender = mock({ + tab: sender.tab, + frameId: 3, + }); + }); + + it("combines the login cipher data from all frames", async () => { + const buildLoginCipherViewSpy = jest.spyOn( + overlayBackground as any, + "buildLoginCipherView", + ); + const addNewCipherType = CipherType.Login; + const topLevelLoginCipherData = { + uri: "https://top-frame-test.com", + hostname: "top-frame-test.com", + username: "", + password: "", + }; + const loginCipherData = { + uri: "https://tacos.com", + hostname: "tacos.com", + username: "username", + password: "", + }; + const subFrameLoginCipherData = { + uri: "https://tacos.com", + hostname: "tacos.com", + username: "", + password: "password", + }; + + sendMockExtensionMessage( + { command, addNewCipherType, login: topLevelLoginCipherData }, + sender, + ); + sendMockExtensionMessage( + { command, addNewCipherType, login: loginCipherData }, + subFrameSender, + ); + sendMockExtensionMessage( + { command, addNewCipherType, login: subFrameLoginCipherData }, + secondSubFrameSender, + ); + jest.advanceTimersByTime(100); + await flushPromises(); + + expect(buildLoginCipherViewSpy).toHaveBeenCalledWith({ + uri: "https://top-frame-test.com", + hostname: "top-frame-test.com", + username: "username", + password: "password", + }); + }); + + it("sets the uri to the subframe of a tab if the login data is complete", async () => { + const buildLoginCipherViewSpy = jest.spyOn( + overlayBackground as any, + "buildLoginCipherView", + ); + const addNewCipherType = CipherType.Login; + const loginCipherData = { + uri: "https://tacos.com", + hostname: "tacos.com", + username: "username", + password: "password", + }; + const topLevelLoginCipherData = { + uri: "https://top-frame-test.com", + hostname: "top-frame-test.com", + username: "", + password: "", + }; + + sendMockExtensionMessage( + { command, addNewCipherType, login: loginCipherData }, + subFrameSender, + ); + sendMockExtensionMessage( + { command, addNewCipherType, login: topLevelLoginCipherData }, + sender, + ); + jest.advanceTimersByTime(100); + await flushPromises(); + + expect(buildLoginCipherViewSpy).toHaveBeenCalledWith({ + uri: "https://tacos.com", + hostname: "tacos.com", + username: "username", + password: "password", + }); + }); + + it("combines the card cipher data from all frames", async () => { + const buildCardCipherViewSpy = jest.spyOn( + overlayBackground as any, + "buildCardCipherView", + ); + overlayBackground["currentAddNewItemData"].addNewCipherType = CipherType.Card; + const addNewCipherType = CipherType.Card; + const cardCipherData = { + cardholderName: "cardholderName", + number: "", + expirationMonth: "", + expirationYear: "", + expirationDate: "12/25", + cvv: "123", + }; + const subFrameCardCipherData = { + cardholderName: "", + number: "4242424242424242", + expirationMonth: "12", + expirationYear: "2025", + expirationDate: "", + cvv: "", + }; + + sendMockExtensionMessage({ command, addNewCipherType, card: cardCipherData }, sender); + sendMockExtensionMessage( + { command, addNewCipherType, card: subFrameCardCipherData }, + subFrameSender, + ); + jest.advanceTimersByTime(100); + await flushPromises(); + + expect(buildCardCipherViewSpy).toHaveBeenCalledWith({ + cardholderName: "cardholderName", + number: "4242424242424242", + expirationMonth: "12", + expirationYear: "2025", + expirationDate: "12/25", + cvv: "123", + }); + }); + + it("combines the identity cipher data from all frames", async () => { + const buildIdentityCipherViewSpy = jest.spyOn( + overlayBackground as any, + "buildIdentityCipherView", + ); + overlayBackground["currentAddNewItemData"].addNewCipherType = CipherType.Identity; + const addNewCipherType = CipherType.Identity; + const identityCipherData = { + title: "title", + firstName: "firstName", + middleName: "middleName", + lastName: "", + fullName: "", + address1: "address1", + address2: "address2", + address3: "address3", + city: "city", + state: "state", + postalCode: "postalCode", + country: "country", + company: "company", + phone: "phone", + email: "email", + username: "username", + }; + const subFrameIdentityCipherData = { + title: "", + firstName: "", + middleName: "", + lastName: "lastName", + fullName: "fullName", + address1: "", + address2: "", + address3: "", + city: "", + state: "", + postalCode: "", + country: "", + company: "", + phone: "", + email: "", + username: "", + }; + + sendMockExtensionMessage( + { command, addNewCipherType, identity: identityCipherData }, + sender, + ); + sendMockExtensionMessage( + { command, addNewCipherType, identity: subFrameIdentityCipherData }, + subFrameSender, + ); + jest.advanceTimersByTime(100); + await flushPromises(); + + expect(buildIdentityCipherViewSpy).toHaveBeenCalledWith({ + title: "title", + firstName: "firstName", + middleName: "middleName", + lastName: "lastName", + fullName: "fullName", + address1: "address1", + address2: "address2", + address3: "address3", + city: "city", + state: "state", + postalCode: "postalCode", + country: "country", + company: "company", + phone: "phone", + email: "email", + username: "username", + }); + }); + }); + }); + + describe("checkIsInlineMenuCiphersPopulated message handler", () => { + let focusedFieldData: FocusedFieldData; + + beforeEach(() => { + focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData }, + mock({ tab: { id: 2 }, frameId: 0 }), + ); + }); + + it("returns false if the sender's tab id is not equal to the focused field's tab id", async () => { + const sender = mock({ tab: { id: 1 } }); + + sendMockExtensionMessage( + { command: "checkIsInlineMenuCiphersPopulated" }, + sender, + sendResponse, + ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(false); + }); + + it("returns false if the overlay login cipher are not populated", () => {}); + + it("returns true if the overlay login ciphers are populated", async () => { + overlayBackground["inlineMenuCiphers"] = new Map([ + ["inline-menu-cipher-0", mock({ type: CipherType.Login })], + ]); + await overlayBackground["getInlineMenuCipherData"](); + + sendMockExtensionMessage( + { command: "checkIsInlineMenuCiphersPopulated" }, + mock({ tab: { id: 2 } }), + sendResponse, + ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(true); + }); + }); + + describe("updateFocusedFieldData message handler", () => { + it("sends a message to the sender frame to unset the most recently focused field data when the currently focused field does not belong to the sender", async () => { + const tab = createChromeTabMock({ id: 2 }); + const firstSender = mock({ tab, frameId: 100 }); + const focusedFieldData = createFocusedFieldDataMock({ + tabId: tab.id, + frameId: firstSender.frameId, + }); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData }, + firstSender, + ); + await flushPromises(); + + const secondSender = mock({ tab, frameId: 10 }); + const otherFocusedFieldData = createFocusedFieldDataMock({ + tabId: tab.id, + frameId: secondSender.frameId, + }); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData: otherFocusedFieldData }, + secondSender, + ); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + tab, + { command: "unsetMostRecentlyFocusedField" }, + { frameId: firstSender.frameId }, + ); + }); + + it("triggers an update of the identity ciphers present on a login field", async () => { + await initOverlayElementPorts(); + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + const tab = createChromeTabMock({ id: 2 }); + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock(); + overlayBackground["isInlineMenuButtonVisible"] = true; + const sender = mock({ tab, frameId: 100 }); + const focusedFieldData = createFocusedFieldDataMock({ + tabId: tab.id, + frameId: sender.frameId, + showInlineMenuAccountCreation: true, + }); + + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + await flushPromises(); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuListCiphers", + ciphers: [], + showInlineMenuAccountCreation: true, + showPasskeysLabels: false, + }); + }); + + it("triggers an update of the inline menu ciphers when the new focused field's cipher type does not equal the previous focused field's cipher type", async () => { + const updateOverlayCiphersSpy = jest.spyOn(overlayBackground, "updateOverlayCiphers"); + const tab = createChromeTabMock({ id: 2 }); + const sender = mock({ tab, frameId: 100 }); + const focusedFieldData = createFocusedFieldDataMock({ + tabId: tab.id, + frameId: sender.frameId, + filledByCipherType: CipherType.Login, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + await flushPromises(); + + const newFocusedFieldData = createFocusedFieldDataMock({ + tabId: tab.id, + frameId: sender.frameId, + filledByCipherType: CipherType.Card, + }); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData: newFocusedFieldData }, + sender, + ); + await flushPromises(); + + expect(updateOverlayCiphersSpy).toHaveBeenCalled(); + }); + }); + + describe("updateIsFieldCurrentlyFocused message handler", () => { + it("skips updating the isFiledCurrentlyFocused value when the focused field data is populated and the sender frame id does not equal the focused field's frame id", async () => { + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData }, + mock({ tab: { id: 1 }, frameId: 10 }), + ); + overlayBackground["isFieldCurrentlyFocused"] = true; + + sendMockExtensionMessage( + { command: "updateIsFieldCurrentlyFocused", isFieldCurrentlyFocused: false }, + mock({ tab: { id: 1 }, frameId: 20 }), + ); + await flushPromises(); + + expect(overlayBackground["isFieldCurrentlyFocused"]).toBe(true); + }); + }); + + describe("updateIsFieldCurrentlyFocused message handler", () => { + it("skips updating the isFiledCurrentlyFocused value when the focused field data is populated and the sender frame id does not equal the focused field's frame id", async () => { + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData }, + mock({ tab: { id: 1 }, frameId: 10 }), + ); + overlayBackground["isFieldCurrentlyFocused"] = true; + + sendMockExtensionMessage( + { command: "updateIsFieldCurrentlyFocused", isFieldCurrentlyFocused: false }, + mock({ tab: { id: 1 }, frameId: 20 }), + ); + await flushPromises(); + + expect(overlayBackground["isFieldCurrentlyFocused"]).toBe(true); + }); + }); + + describe("checkIsFieldCurrentlyFocused message handler", () => { + it("returns true when a form field is currently focused", async () => { + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFocused", + isFieldCurrentlyFocused: true, + }); + + sendMockExtensionMessage( + { command: "checkIsFieldCurrentlyFocused" }, + mock(), + sendResponse, + ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(true); + }); + }); + + describe("checkIsFieldCurrentlyFilling message handler", () => { + it("returns true if autofill is currently running", async () => { + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFilling", + isFieldCurrentlyFilling: true, + }); + + sendMockExtensionMessage( + { command: "checkIsFieldCurrentlyFilling" }, + mock(), + sendResponse, + ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(true); + }); + }); + + describe("getAutofillInlineMenuVisibility message handler", () => { + it("returns the current inline menu visibility setting", async () => { + sendMockExtensionMessage( + { command: "getAutofillInlineMenuVisibility" }, + mock(), + sendResponse, + ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(AutofillOverlayVisibility.OnFieldFocus); + }); + }); + + describe("openAutofillInlineMenu message handler", () => { + let sender: chrome.runtime.MessageSender; + + beforeEach(() => { + sender = mock({ tab: { id: 1 } }); + getTabFromCurrentWindowIdSpy.mockResolvedValue(sender.tab); + tabsSendMessageSpy.mockImplementation(); + }); + + it("opens the autofill inline menu by sending a message to the current tab", async () => { + sendMockExtensionMessage({ command: "openAutofillInlineMenu" }, sender); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "openAutofillInlineMenu", + isFocusingFieldElement: false, + isOpeningFullInlineMenu: false, + authStatus: AuthenticationStatus.Unlocked, + }, + { frameId: 0 }, + ); + }); + + it("sends the open menu message to the focused field's frameId", async () => { + sender.frameId = 10; + sendMockExtensionMessage({ command: "updateFocusedFieldData" }, sender); + await flushPromises(); + + sendMockExtensionMessage({ command: "openAutofillInlineMenu" }, sender); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "openAutofillInlineMenu", + isFocusingFieldElement: false, + isOpeningFullInlineMenu: false, + authStatus: AuthenticationStatus.Unlocked, + }, + { frameId: 10 }, + ); + }); + }); + + describe("closeAutofillInlineMenu", () => { + let sender: chrome.runtime.MessageSender; + + beforeEach(() => { + sender = mock({ tab: { id: 1 } }); + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFilling", + isFieldCurrentlyFilling: false, + }); + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFocused", + isFieldCurrentlyFocused: false, + }); + }); + + it("sends a message to close the inline menu without checking field focus state if forcing the closure", async () => { + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFocused", + isFieldCurrentlyFocused: true, + }); + await flushPromises(); + + sendMockExtensionMessage( + { + command: "closeAutofillInlineMenu", + forceCloseInlineMenu: true, + overlayElement: AutofillOverlayElement.Button, + }, + sender, + ); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "closeAutofillInlineMenu", + overlayElement: AutofillOverlayElement.Button, + }, + { frameId: 0 }, + ); + }); + + it("skips sending a message to close the inline menu if a form field is currently focused", async () => { + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFocused", + isFieldCurrentlyFocused: true, + }); + await flushPromises(); + + sendMockExtensionMessage( + { + command: "closeAutofillInlineMenu", + forceCloseInlineMenu: false, + overlayElement: AutofillOverlayElement.Button, + }, + sender, + ); + await flushPromises(); + + expect(tabsSendMessageSpy).not.toHaveBeenCalled(); + }); + + it("sends a message to close the inline menu list only if the field is currently filling", async () => { + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFilling", + isFieldCurrentlyFilling: true, + }); + await flushPromises(); + + sendMockExtensionMessage({ command: "closeAutofillInlineMenu" }, sender); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "closeAutofillInlineMenu", + overlayElement: AutofillOverlayElement.List, + }, + { frameId: 0 }, + ); + expect(tabsSendMessageSpy).not.toHaveBeenCalledWith( + sender.tab, + { + command: "closeAutofillInlineMenu", + overlayElement: AutofillOverlayElement.Button, + }, + { frameId: 0 }, + ); + }); + + it("sends a message to close the inline menu if the form field is not focused and not filling", async () => { + overlayBackground["isInlineMenuButtonVisible"] = true; + overlayBackground["isInlineMenuListVisible"] = true; + + sendMockExtensionMessage({ command: "closeAutofillInlineMenu" }, sender); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "closeAutofillInlineMenu", + overlayElement: undefined, + }, + { frameId: 0 }, + ); + expect(overlayBackground["isInlineMenuButtonVisible"]).toBe(false); + expect(overlayBackground["isInlineMenuListVisible"]).toBe(false); + }); + + it("sets a property indicating that the inline menu button is not visible", async () => { + overlayBackground["isInlineMenuButtonVisible"] = true; + + sendMockExtensionMessage( + { command: "closeAutofillInlineMenu", overlayElement: AutofillOverlayElement.Button }, + sender, + ); + await flushPromises(); + + expect(overlayBackground["isInlineMenuButtonVisible"]).toBe(false); + }); + + it("sets a property indicating that the inline menu list is not visible", async () => { + overlayBackground["isInlineMenuListVisible"] = true; + + sendMockExtensionMessage( + { command: "closeAutofillInlineMenu", overlayElement: AutofillOverlayElement.List }, + sender, + ); + await flushPromises(); + + expect(overlayBackground["isInlineMenuListVisible"]).toBe(false); + }); + }); + + describe("checkAutofillInlineMenuFocused message handler", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + }); + + it("skips checking if the inline menu is focused if the sender does not contain the focused field", async () => { + const sender = mock({ tab: { id: 1 } }); + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ command: "checkAutofillInlineMenuFocused" }, sender); + + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuListFocused", + }); + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuButtonFocused", + }); + }); + + it("will check if the inline menu list is focused if the list port is open", () => { + sendMockExtensionMessage({ command: "checkAutofillInlineMenuFocused" }); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuListFocused", + }); + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuButtonFocused", + }); + }); + + it("will check if the overlay button is focused if the list port is not open", () => { + overlayBackground["inlineMenuListPort"] = undefined; + + sendMockExtensionMessage({ command: "checkAutofillInlineMenuFocused" }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuButtonFocused", + }); + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuListFocused", + }); + }); + }); + + describe("focusAutofillInlineMenuList message handler", () => { + it("will send a `focusInlineMenuList` message to the overlay list port", async () => { + await initOverlayElementPorts({ initList: true, initButton: false }); + + sendMockExtensionMessage({ command: "focusAutofillInlineMenuList" }); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "focusAutofillInlineMenuList", + }); + }); + }); + + describe("updateAutofillInlineMenuPosition message handler", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + }); + + it("ignores updating the position if the overlay element type is not provided", () => { + sendMockExtensionMessage({ command: "updateAutofillInlineMenuPosition" }); + + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + }); + + it("skips updating the position if the most recently focused field is different than the message sender", () => { + const sender = mock({ tab: { id: 1 } }); + const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ command: "updateAutofillInlineMenuPosition" }, sender); + + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + }); + + it("updates the inline menu button's position", async () => { + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillInlineMenuPosition", + overlayElement: AutofillOverlayElement.Button, + }); + await flushPromises(); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuPosition", + styles: { height: "2px", left: "4px", top: "2px", width: "2px" }, + }); + }); + + it("modifies the inline menu button's height for medium sized input elements", async () => { + const focusedFieldData = createFocusedFieldDataMock({ + focusedFieldRects: { top: 1, left: 2, height: 35, width: 4 }, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillInlineMenuPosition", + overlayElement: AutofillOverlayElement.Button, + }); + await flushPromises(); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuPosition", + styles: { height: "20px", left: "-22px", top: "8px", width: "20px" }, + }); + }); + + it("modifies the inline menu button's height for large sized input elements", async () => { + const focusedFieldData = createFocusedFieldDataMock({ + focusedFieldRects: { top: 1, left: 2, height: 50, width: 4 }, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillInlineMenuPosition", + overlayElement: AutofillOverlayElement.Button, + }); + await flushPromises(); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuPosition", + styles: { height: "27px", left: "-32px", top: "13px", width: "27px" }, + }); + }); + + it("takes into account the right padding of the focused field in positioning the button if the right padding of the field is larger than the left padding", async () => { + const focusedFieldData = createFocusedFieldDataMock({ + focusedFieldStyles: { paddingRight: "20px", paddingLeft: "6px" }, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillInlineMenuPosition", + overlayElement: AutofillOverlayElement.Button, + }); + await flushPromises(); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuPosition", + styles: { height: "2px", left: "-18px", top: "2px", width: "2px" }, + }); + }); + + it("updates the inline menu list's position", async () => { + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillInlineMenuPosition", + overlayElement: AutofillOverlayElement.List, + }); + await flushPromises(); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuPosition", + styles: { left: "2px", top: "4px", width: "4px" }, + }); + }); + + it("sends a message that triggers a simultaneous fade in for both inline menu elements", async () => { + jest.useFakeTimers(); + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillInlineMenuPosition", + overlayElement: AutofillOverlayElement.List, + }); + await flushPromises(); + jest.advanceTimersByTime(150); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "fadeInAutofillInlineMenuIframe", + }); + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "fadeInAutofillInlineMenuIframe", + }); + }); + + describe("getAutofillInlineMenuPosition", () => { + it("returns the current inline menu position", async () => { + overlayBackground["inlineMenuPosition"] = { + button: { left: 1, top: 2, width: 3, height: 4 }, + }; + + sendMockExtensionMessage( + { command: "getAutofillInlineMenuPosition" }, + mock(), + sendResponse, + ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith({ + button: { left: 1, top: 2, width: 3, height: 4 }, + }); + }); + }); + + it("triggers a debounced reposition of the inline menu if the sender frame has a `null` sub frame offsets value", async () => { + jest.useFakeTimers(); + const focusedFieldData = createFocusedFieldDataMock(); + const sender = mock({ + tab: { id: focusedFieldData.tabId }, + frameId: focusedFieldData.frameId, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + overlayBackground["subFrameOffsetsForTab"][focusedFieldData.tabId] = new Map([ + [focusedFieldData.frameId, null], + ]); + jest.spyOn(overlayBackground as any, "updateInlineMenuPositionAfterRepositionEvent"); + + sendMockExtensionMessage( + { + command: "updateAutofillInlineMenuPosition", + overlayElement: AutofillOverlayElement.List, + }, + sender, + ); + await flushPromises(); + jest.advanceTimersByTime(150); + + expect( + overlayBackground["updateInlineMenuPositionAfterRepositionEvent"], + ).toHaveBeenCalled(); + }); + }); + + describe("updateAutofillInlineMenuElementIsVisibleStatus message handler", () => { + let sender: chrome.runtime.MessageSender; + let focusedFieldData: FocusedFieldData; + + beforeEach(() => { + sender = mock({ tab: { id: 1 } }); + focusedFieldData = createFocusedFieldDataMock(); + overlayBackground["isInlineMenuButtonVisible"] = true; + overlayBackground["isInlineMenuListVisible"] = false; + }); + + it("skips updating the inline menu visibility status if the sender tab does not contain the focused field", async () => { + const otherSender = mock({ tab: { id: 2 } }); + sendMockExtensionMessage( + { command: "updateFocusedFieldData", focusedFieldData }, + otherSender, + ); + + sendMockExtensionMessage( + { + command: "updateAutofillInlineMenuElementIsVisibleStatus", + overlayElement: AutofillOverlayElement.Button, + isVisible: false, + }, + sender, + ); + + expect(overlayBackground["isInlineMenuButtonVisible"]).toBe(true); + expect(overlayBackground["isInlineMenuListVisible"]).toBe(false); + }); + + it("updates the visibility status of the inline menu button", async () => { + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + + sendMockExtensionMessage( + { + command: "updateAutofillInlineMenuElementIsVisibleStatus", + overlayElement: AutofillOverlayElement.Button, + isVisible: false, + }, + sender, + ); + + expect(overlayBackground["isInlineMenuButtonVisible"]).toBe(false); + expect(overlayBackground["isInlineMenuListVisible"]).toBe(false); + }); + + it("updates the visibility status of the inline menu list", async () => { + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + + sendMockExtensionMessage( + { + command: "updateAutofillInlineMenuElementIsVisibleStatus", + overlayElement: AutofillOverlayElement.List, + isVisible: true, + }, + sender, + ); + + expect(overlayBackground["isInlineMenuButtonVisible"]).toBe(true); + expect(overlayBackground["isInlineMenuListVisible"]).toBe(true); + }); + }); + + describe("checkIsAutofillInlineMenuButtonVisible message handler", () => { + it("returns true when the inline menu button is visible", async () => { + overlayBackground["isInlineMenuButtonVisible"] = true; + const sender = mock({ tab: { id: 1 } }); + + sendMockExtensionMessage( + { command: "checkIsAutofillInlineMenuButtonVisible" }, + sender, + sendResponse, + ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(true); + }); + }); + + describe("checkIsAutofillInlineMenuListVisible message handler", () => { + it("returns true when the inline menu list is visible", async () => { + overlayBackground["isInlineMenuListVisible"] = true; + const sender = mock({ tab: { id: 1 } }); + + sendMockExtensionMessage( + { command: "checkIsAutofillInlineMenuListVisible" }, + sender, + sendResponse, + ); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(true); + }); + }); + + describe("getCurrentTabFrameId message handler", () => { + it("returns the sender's frame id", async () => { + const sender = mock({ frameId: 1 }); + + sendMockExtensionMessage({ command: "getCurrentTabFrameId" }, sender, sendResponse); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith(1); + }); + }); + + describe("destroyAutofillInlineMenuListeners", () => { + it("sends a message to the passed frameId that triggers a destruction of the inline menu listeners on that frame", () => { + const sender = mock({ tab: { id: 1 }, frameId: 0 }); + + sendMockExtensionMessage( + { command: "destroyAutofillInlineMenuListeners", subFrameData: { frameId: 10 } }, + sender, + ); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { command: "destroyAutofillInlineMenuListeners" }, + { frameId: 10 }, + ); + }); + }); + + describe("unlockCompleted", () => { + let updateInlineMenuCiphersSpy: jest.SpyInstance; + + beforeEach(async () => { + updateInlineMenuCiphersSpy = jest.spyOn(overlayBackground, "updateOverlayCiphers"); + await initOverlayElementPorts(); + }); + + it("updates the inline menu button auth status", async () => { + sendMockExtensionMessage({ command: "unlockCompleted" }); + await flushPromises(); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateInlineMenuButtonAuthStatus", + authStatus: AuthenticationStatus.Unlocked, + }); + }); + + it("updates the overlay ciphers", async () => { + const updateInlineMenuCiphersSpy = jest.spyOn(overlayBackground, "updateOverlayCiphers"); + sendMockExtensionMessage({ command: "unlockCompleted" }); + await flushPromises(); + + expect(updateInlineMenuCiphersSpy).toHaveBeenCalled(); + }); + + it("opens the inline menu if a retry command is present in the message", async () => { + updateInlineMenuCiphersSpy.mockImplementation(); + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(createChromeTabMock({ id: 1 })); + sendMockExtensionMessage({ + command: "unlockCompleted", + data: { + commandToRetry: { message: { command: "openAutofillInlineMenu" } }, + }, + }); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + expect.any(Object), + { + command: "openAutofillInlineMenu", + isFocusingFieldElement: true, + isOpeningFullInlineMenu: false, + authStatus: AuthenticationStatus.Unlocked, + }, + { frameId: 0 }, + ); + }); + }); + + describe("extension messages that trigger an update of the inline menu ciphers", () => { + const extensionMessages = [ + "doFullSync", + "addedCipher", + "addEditCipherSubmitted", + "editedCipher", + "deletedCipher", ]; - translationKeys.forEach((key) => { - expect(overlayBackground["i18nService"].translate).toHaveBeenCalledWith(key); + + beforeEach(() => { + jest.spyOn(overlayBackground, "updateOverlayCiphers").mockImplementation(); }); - expect(translations).toStrictEqual({ - locale: "en", - opensInANewWindow: "opensInANewWindow", - buttonPageTitle: "bitwardenOverlayButton", - toggleBitwardenVaultOverlay: "toggleBitwardenVaultOverlay", - listPageTitle: "bitwardenVault", - unlockYourAccount: "unlockYourAccountToViewMatchingLogins", - unlockAccount: "unlockAccount", - fillCredentialsFor: "fillCredentialsFor", - partialUsername: "partialUsername", - view: "view", - noItemsToShow: "noItemsToShow", - newItem: "newItem", - addNewVaultItem: "addNewVaultItem", + + extensionMessages.forEach((message) => { + it(`triggers an update of the overlay ciphers when the ${message} message is received`, () => { + sendMockExtensionMessage({ command: message }); + expect(overlayBackground.updateOverlayCiphers).toHaveBeenCalled(); + }); + }); + }); + + describe("fido2AbortRequest", () => { + const sender = mock({ tab: { id: 1 } }); + it("removes an active request associated with the sender tab", () => { + const removeActiveRequestSpy = jest.spyOn(fido2ActiveRequestManager, "removeActiveRequest"); + + sendMockExtensionMessage({ command: "fido2AbortRequest" }, sender); + + expect(removeActiveRequestSpy).toHaveBeenCalledWith(sender.tab.id); + }); + + it("updates the overlay ciphers after removing the active request", () => { + const updateOverlayCiphersSpy = jest.spyOn(overlayBackground, "updateOverlayCiphers"); + + sendMockExtensionMessage({ command: "fido2AbortRequest" }, sender); + + expect(updateOverlayCiphersSpy).toHaveBeenCalledWith(false); }); }); }); - describe("setupExtensionMessageListeners", () => { - it("will set up onMessage and onConnect listeners", () => { - overlayBackground["setupExtensionMessageListeners"](); - - // eslint-disable-next-line - expect(chrome.runtime.onMessage.addListener).toHaveBeenCalled(); - expect(chrome.runtime.onConnect.addListener).toHaveBeenCalled(); - }); - }); - - describe("handleExtensionMessage", () => { + describe("handle extension onMessage", () => { it("will return early if the message command is not present within the extensionMessageHandlers", () => { const message = { command: "not-a-command", @@ -497,970 +2656,641 @@ describe("OverlayBackground", () => { sendResponse, ); - expect(returnValue).toBe(undefined); + expect(returnValue).toBe(null); expect(sendResponse).not.toHaveBeenCalled(); }); + }); - it("will trigger the message handler and return undefined if the message does not have a response", () => { - const message = { - command: "autofillOverlayElementClosed", - }; - const sender = mock({ tab: { id: 1 } }); - const sendResponse = jest.fn(); - jest.spyOn(overlayBackground as any, "overlayElementClosed"); + describe("inline menu button message handlers", () => { + let sender: chrome.runtime.MessageSender; + const portKey = "inlineMenuButtonPort"; - const returnValue = overlayBackground["handleExtensionMessage"]( - message, - sender, - sendResponse, - ); - - expect(returnValue).toBe(undefined); - expect(sendResponse).not.toHaveBeenCalled(); - expect(overlayBackground["overlayElementClosed"]).toHaveBeenCalledWith(message, sender); + beforeEach(async () => { + sender = mock({ tab: { id: 1 } }); + portKeyForTabSpy[sender.tab.id] = portKey; + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + await initOverlayElementPorts(); + buttonMessageConnectorSpy.sender = sender; + openUnlockPopoutSpy.mockImplementation(); }); - it("will return a response if the message handler returns a response", async () => { - const message = { - command: "openAutofillOverlay", - }; - const sender = mock({ tab: { id: 1 } }); - const sendResponse = jest.fn(); - jest.spyOn(overlayBackground as any, "getTranslations").mockReturnValue("translations"); + describe("autofillInlineMenuButtonClicked message handler", () => { + it("opens the unlock vault popout if the user auth status is not unlocked", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Locked); - const returnValue = overlayBackground["handleExtensionMessage"]( - message, - sender, - sendResponse, - ); + sendPortMessage(buttonMessageConnectorSpy, { + command: "autofillInlineMenuButtonClicked", + portKey, + }); + await flushPromises(); - expect(returnValue).toBe(true); + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { command: "closeAutofillInlineMenu", overlayElement: undefined }, + { frameId: 0 }, + ); + expect(tabSendMessageDataSpy).toBeCalledWith( + sender.tab, + "addToLockedVaultPendingNotifications", + { + commandToRetry: { message: { command: "openAutofillInlineMenu" }, sender }, + target: "overlay.background", + }, + ); + expect(openUnlockPopoutSpy).toHaveBeenCalled(); + }); + + it("opens the inline menu if the user auth status is unlocked", async () => { + getTabFromCurrentWindowIdSpy.mockResolvedValueOnce(sender.tab); + sendPortMessage(buttonMessageConnectorSpy, { + command: "autofillInlineMenuButtonClicked", + portKey, + }); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith( + sender.tab, + { + command: "openAutofillInlineMenu", + isFocusingFieldElement: false, + isOpeningFullInlineMenu: true, + authStatus: AuthenticationStatus.Unlocked, + }, + { frameId: 0 }, + ); + }); }); - describe("extension message handlers", () => { - beforeEach(() => { - jest - .spyOn(overlayBackground as any, "getAuthStatus") - .mockResolvedValue(AuthenticationStatus.Unlocked); + describe("triggerDelayedAutofillInlineMenuClosure message handler", () => { + it("skips triggering the delayed closure of the inline menu if a field is currently focused", async () => { + jest.useFakeTimers(); + sendMockExtensionMessage({ + command: "updateIsFieldCurrentlyFocused", + isFieldCurrentlyFocused: true, + }); + await flushPromises(); + + sendPortMessage(buttonMessageConnectorSpy, { + command: "triggerDelayedAutofillInlineMenuClosure", + portKey, + }); + await flushPromises(); + jest.advanceTimersByTime(100); + + const message = { command: "triggerDelayedAutofillInlineMenuClosure" }; + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith(message); + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith(message); }); - describe("openAutofillOverlay message handler", () => { - it("opens the autofill overlay by sending a message to the current tab", async () => { - const sender = mock({ tab: { id: 1 } }); - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(sender.tab); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); - - sendMockExtensionMessage({ command: "openAutofillOverlay" }); - await flushPromises(); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - sender.tab, - "openAutofillOverlay", - { - isFocusingFieldElement: false, - isOpeningFullOverlay: false, - authStatus: AuthenticationStatus.Unlocked, - }, - ); + it("sends a message to the button and list ports that triggers a delayed closure of the inline menu", async () => { + jest.useFakeTimers(); + sendPortMessage(buttonMessageConnectorSpy, { + command: "triggerDelayedAutofillInlineMenuClosure", + portKey, }); + await flushPromises(); + jest.advanceTimersByTime(100); + + const message = { command: "triggerDelayedAutofillInlineMenuClosure" }; + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith(message); + expect(listPortSpy.postMessage).toHaveBeenCalledWith(message); }); - describe("autofillOverlayElementClosed message handler", () => { - beforeEach(async () => { - await initOverlayElementPorts(); + it("triggers a single delayed closure if called again within a 100ms threshold", async () => { + jest.useFakeTimers(); + sendPortMessage(buttonMessageConnectorSpy, { + command: "triggerDelayedAutofillInlineMenuClosure", + portKey, }); - - it("disconnects any expired ports if the sender is not from the same page as the most recently focused field", () => { - const port1 = mock(); - const port2 = mock(); - overlayBackground["expiredPorts"] = [port1, port2]; - const sender = mock({ tab: { id: 1 } }); - const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage( - { - command: "autofillOverlayElementClosed", - overlayElement: AutofillOverlayElement.Button, - }, - sender, - ); - - expect(port1.disconnect).toHaveBeenCalled(); - expect(port2.disconnect).toHaveBeenCalled(); + await flushPromises(); + jest.advanceTimersByTime(50); + sendPortMessage(buttonMessageConnectorSpy, { + command: "triggerDelayedAutofillInlineMenuClosure", + portKey, }); + await flushPromises(); + jest.advanceTimersByTime(100); - it("disconnects the button element port", () => { - sendMockExtensionMessage({ - command: "autofillOverlayElementClosed", - overlayElement: AutofillOverlayElement.Button, - }); + const message = { command: "triggerDelayedAutofillInlineMenuClosure" }; + expect(buttonPortSpy.postMessage).toHaveBeenCalledTimes(2); + expect(buttonPortSpy.postMessage).not.toHaveBeenNthCalledWith(1, message); + expect(buttonPortSpy.postMessage).toHaveBeenNthCalledWith(2, message); + expect(listPortSpy.postMessage).toHaveBeenCalledTimes(2); + expect(listPortSpy.postMessage).not.toHaveBeenNthCalledWith(1, message); + expect(listPortSpy.postMessage).toHaveBeenNthCalledWith(2, message); + }); + }); - expect(buttonPortSpy.disconnect).toHaveBeenCalled(); - expect(overlayBackground["overlayButtonPort"]).toBeNull(); + describe("autofillInlineMenuBlurred message handler", () => { + it("sends a message to the inline menu list to check if the element is focused", async () => { + sendPortMessage(buttonMessageConnectorSpy, { + command: "autofillInlineMenuBlurred", + portKey, }); + await flushPromises(); - it("disconnects the list element port", () => { - sendMockExtensionMessage({ - command: "autofillOverlayElementClosed", - overlayElement: AutofillOverlayElement.List, - }); - - expect(listPortSpy.disconnect).toHaveBeenCalled(); - expect(overlayBackground["overlayListPort"]).toBeNull(); + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuListFocused", }); }); + }); - describe("autofillOverlayAddNewVaultItem message handler", () => { - let sender: chrome.runtime.MessageSender; - beforeEach(() => { - sender = mock({ tab: { id: 1 } }); - jest - .spyOn(overlayBackground["cipherService"], "setAddEditCipherInfo") - .mockImplementation(); - jest.spyOn(overlayBackground as any, "openAddEditVaultItemPopout").mockImplementation(); + describe("redirectAutofillInlineMenuFocusOut message handler", () => { + it("ignores the redirect message if the direction is not provided", () => { + sendPortMessage(buttonMessageConnectorSpy, { + command: "redirectAutofillInlineMenuFocusOut", + portKey, }); - it("will not open the add edit popout window if the message does not have a login cipher provided", () => { - sendMockExtensionMessage({ command: "autofillOverlayAddNewVaultItem" }, sender); - - expect(overlayBackground["cipherService"].setAddEditCipherInfo).not.toHaveBeenCalled(); - expect(overlayBackground["openAddEditVaultItemPopout"]).not.toHaveBeenCalled(); - }); - - it("will open the add edit popout window after creating a new cipher", async () => { - jest.spyOn(BrowserApi, "sendMessage"); - - sendMockExtensionMessage( - { - command: "autofillOverlayAddNewVaultItem", - login: { - uri: "https://tacos.com", - hostname: "", - username: "username", - password: "password", - }, - }, - sender, - ); - await flushPromises(); - - expect(overlayBackground["cipherService"].setAddEditCipherInfo).toHaveBeenCalled(); - expect(BrowserApi.sendMessage).toHaveBeenCalledWith( - "inlineAutofillMenuRefreshAddEditCipher", - ); - expect(overlayBackground["openAddEditVaultItemPopout"]).toHaveBeenCalled(); - }); + expect(tabSendMessageDataSpy).not.toHaveBeenCalled(); }); - describe("getAutofillOverlayVisibility message handler", () => { - beforeEach(() => { - jest - .spyOn(overlayBackground as any, "getOverlayVisibility") - .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); + it("sends the redirect message if the direction is provided", () => { + sendPortMessage(buttonMessageConnectorSpy, { + command: "redirectAutofillInlineMenuFocusOut", + direction: RedirectFocusDirection.Next, + portKey, }); - it("will set the overlayVisibility property", async () => { - sendMockExtensionMessage({ command: "getAutofillOverlayVisibility" }); - await flushPromises(); - - expect(await overlayBackground["getOverlayVisibility"]()).toBe( - AutofillOverlayVisibility.OnFieldFocus, - ); - }); - - it("returns the overlayVisibility property", async () => { - const sendMessageSpy = jest.fn(); - - sendMockExtensionMessage( - { command: "getAutofillOverlayVisibility" }, - undefined, - sendMessageSpy, - ); - await flushPromises(); - - expect(sendMessageSpy).toHaveBeenCalledWith(AutofillOverlayVisibility.OnFieldFocus); - }); + expect(tabSendMessageDataSpy).toHaveBeenCalledWith( + sender.tab, + "redirectAutofillInlineMenuFocusOut", + { direction: RedirectFocusDirection.Next }, + ); }); + }); - describe("checkAutofillOverlayFocused message handler", () => { - beforeEach(async () => { - await initOverlayElementPorts(); + describe("updateAutofillInlineMenuColorScheme message handler", () => { + it("sends a message to the button port to update the inline menu color scheme", async () => { + sendPortMessage(buttonMessageConnectorSpy, { + command: "updateAutofillInlineMenuColorScheme", + portKey, }); + await flushPromises(); - it("will check if the overlay list is focused if the list port is open", () => { - sendMockExtensionMessage({ command: "checkAutofillOverlayFocused" }); - - expect(listPortSpy.postMessage).toHaveBeenCalledWith({ - command: "checkAutofillOverlayListFocused", - }); - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "checkAutofillOverlayButtonFocused", - }); - }); - - it("will check if the overlay button is focused if the list port is not open", () => { - overlayBackground["overlayListPort"] = undefined; - - sendMockExtensionMessage({ command: "checkAutofillOverlayFocused" }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "checkAutofillOverlayButtonFocused", - }); - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "checkAutofillOverlayListFocused", - }); - }); - }); - - describe("focusAutofillOverlayList message handler", () => { - it("will send a `focusOverlayList` message to the overlay list port", async () => { - await initOverlayElementPorts({ initList: true, initButton: false }); - - sendMockExtensionMessage({ command: "focusAutofillOverlayList" }); - - expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "focusOverlayList" }); - }); - }); - - describe("updateAutofillOverlayPosition message handler", () => { - beforeEach(async () => { - await overlayBackground["handlePortOnConnect"]( - createPortSpyMock(AutofillOverlayPort.List), - ); - listPortSpy = overlayBackground["overlayListPort"]; - - await overlayBackground["handlePortOnConnect"]( - createPortSpyMock(AutofillOverlayPort.Button), - ); - buttonPortSpy = overlayBackground["overlayButtonPort"]; - }); - - it("ignores updating the position if the overlay element type is not provided", () => { - sendMockExtensionMessage({ command: "updateAutofillOverlayPosition" }); - - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - }); - - it("skips updating the position if the most recently focused field is different than the message sender", () => { - const sender = mock({ tab: { id: 1 } }); - const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ command: "updateAutofillOverlayPosition" }, sender); - - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: expect.anything(), - }); - }); - - it("updates the overlay button's position", () => { - const focusedFieldData = createFocusedFieldDataMock(); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "2px", left: "4px", top: "2px", width: "2px" }, - }); - }); - - it("modifies the overlay button's height for medium sized input elements", () => { - const focusedFieldData = createFocusedFieldDataMock({ - focusedFieldRects: { top: 1, left: 2, height: 35, width: 4 }, - }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "20px", left: "-22px", top: "8px", width: "20px" }, - }); - }); - - it("modifies the overlay button's height for large sized input elements", () => { - const focusedFieldData = createFocusedFieldDataMock({ - focusedFieldRects: { top: 1, left: 2, height: 50, width: 4 }, - }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "27px", left: "-32px", top: "13px", width: "27px" }, - }); - }); - - it("takes into account the right padding of the focused field in positioning the button if the right padding of the field is larger than the left padding", () => { - const focusedFieldData = createFocusedFieldDataMock({ - focusedFieldStyles: { paddingRight: "20px", paddingLeft: "6px" }, - }); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.Button, - }); - - expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { height: "2px", left: "-18px", top: "2px", width: "2px" }, - }); - }); - - it("will post a message to the overlay list facilitating an update of the list's position", () => { - const sender = mock({ tab: { id: 1 } }); - const focusedFieldData = createFocusedFieldDataMock(); - sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); - - overlayBackground["updateOverlayPosition"]( - { overlayElement: AutofillOverlayElement.List }, - sender, - ); - sendMockExtensionMessage({ - command: "updateAutofillOverlayPosition", - overlayElement: AutofillOverlayElement.List, - }); - - expect(listPortSpy.postMessage).toHaveBeenCalledWith({ - command: "updateIframePosition", - styles: { left: "2px", top: "4px", width: "4px" }, - }); - }); - }); - - describe("updateOverlayHidden", () => { - beforeEach(async () => { - await initOverlayElementPorts(); - }); - - it("returns early if the display value is not provided", () => { - const message = { - command: "updateAutofillOverlayHidden", - }; - - sendMockExtensionMessage(message); - - expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith(message); - expect(listPortSpy.postMessage).not.toHaveBeenCalledWith(message); - }); - - it("posts a message to the overlay button and list with the display value", () => { - const message = { command: "updateAutofillOverlayHidden", display: "none" }; - - sendMockExtensionMessage(message); - - expect(overlayBackground["overlayButtonPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayHidden", - styles: { - display: message.display, - }, - }); - expect(overlayBackground["overlayListPort"].postMessage).toHaveBeenCalledWith({ - command: "updateOverlayHidden", - styles: { - display: message.display, - }, - }); - }); - }); - - describe("collectPageDetailsResponse message handler", () => { - let sender: chrome.runtime.MessageSender; - const pageDetails1 = createAutofillPageDetailsMock({ - login: { username: "username1", password: "password1" }, - }); - const pageDetails2 = createAutofillPageDetailsMock({ - login: { username: "username2", password: "password2" }, - }); - - beforeEach(() => { - sender = mock({ tab: { id: 1 } }); - }); - - it("stores the page details provided by the message by the tab id of the sender", () => { - sendMockExtensionMessage( - { command: "collectPageDetailsResponse", details: pageDetails1 }, - sender, - ); - - expect(overlayBackground["pageDetailsForTab"][sender.tab.id]).toStrictEqual( - new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], - ]), - ); - }); - - it("updates the page details for a tab that already has a set of page details stored ", () => { - const secondFrameSender = mock({ - tab: { id: 1 }, - frameId: 3, - }); - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], - ]); - - sendMockExtensionMessage( - { command: "collectPageDetailsResponse", details: pageDetails2 }, - secondFrameSender, - ); - - expect(overlayBackground["pageDetailsForTab"][sender.tab.id]).toStrictEqual( - new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], - [ - secondFrameSender.frameId, - { - frameId: secondFrameSender.frameId, - tab: secondFrameSender.tab, - details: pageDetails2, - }, - ], - ]), - ); - }); - }); - - describe("unlockCompleted message handler", () => { - let getAuthStatusSpy: jest.SpyInstance; - - beforeEach(() => { - overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; - jest.spyOn(BrowserApi, "tabSendMessageData"); - getAuthStatusSpy = jest - .spyOn(overlayBackground as any, "getAuthStatus") - .mockImplementation(() => { - overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; - return Promise.resolve(AuthenticationStatus.Unlocked); - }); - }); - - it("updates the user's auth status but does not open the overlay", async () => { - const message = { - command: "unlockCompleted", - data: { - commandToRetry: { message: { command: "" } }, - }, - }; - - sendMockExtensionMessage(message); - await flushPromises(); - - expect(getAuthStatusSpy).toHaveBeenCalled(); - expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled(); - }); - - it("updates user's auth status and opens the overlay if a follow up command is provided", async () => { - const sender = mock({ tab: { id: 1 } }); - const message = { - command: "unlockCompleted", - data: { - commandToRetry: { message: { command: "openAutofillOverlay" } }, - }, - }; - jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(sender.tab); - - sendMockExtensionMessage(message); - await flushPromises(); - - expect(getAuthStatusSpy).toHaveBeenCalled(); - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - sender.tab, - "openAutofillOverlay", - { - isFocusingFieldElement: true, - isOpeningFullOverlay: false, - authStatus: AuthenticationStatus.Unlocked, - }, - ); - }); - }); - - describe("extension messages that trigger an update of the inline menu ciphers", () => { - const extensionMessages = [ - "addedCipher", - "addEditCipherSubmitted", - "editedCipher", - "deletedCipher", - ]; - - beforeEach(() => { - jest.spyOn(overlayBackground, "updateOverlayCiphers").mockImplementation(); - }); - - extensionMessages.forEach((message) => { - it(`triggers an update of the overlay ciphers when the ${message} message is received`, () => { - sendMockExtensionMessage({ command: message }); - expect(overlayBackground.updateOverlayCiphers).toHaveBeenCalled(); - }); + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuColorScheme", }); }); }); }); - describe("handlePortOnConnect", () => { - beforeEach(() => { - jest.spyOn(overlayBackground as any, "updateOverlayPosition").mockImplementation(); - jest.spyOn(overlayBackground as any, "getAuthStatus").mockImplementation(); - jest.spyOn(overlayBackground as any, "getTranslations").mockImplementation(); - jest.spyOn(overlayBackground as any, "getOverlayCipherData").mockImplementation(); + describe("inline menu list message handlers", () => { + let sender: chrome.runtime.MessageSender; + const portKey = "inlineMenuListPort"; + + beforeEach(async () => { + sender = mock({ tab: { id: 1 } }); + portKeyForTabSpy[sender.tab.id] = portKey; + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + await initOverlayElementPorts(); + listMessageConnectorSpy.sender = sender; + openUnlockPopoutSpy.mockImplementation(); }); + describe("checkAutofillInlineMenuButtonFocused message handler", () => { + it("sends a message to the inline menu button to check if the element is focused", async () => { + sendPortMessage(listMessageConnectorSpy, { + command: "checkAutofillInlineMenuButtonFocused", + portKey, + }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuButtonFocused", + }); + }); + }); + + describe("autofillInlineMenuBlurred message handler", () => { + it("sends a message to the inline menu button to check if the element is focused", async () => { + sendPortMessage(listMessageConnectorSpy, { + command: "autofillInlineMenuBlurred", + portKey, + }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuButtonFocused", + }); + }); + }); + + describe("unlockVault message handler", () => { + it("opens the unlock vault popout", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Locked); + + sendPortMessage(listMessageConnectorSpy, { command: "unlockVault", portKey }); + await flushPromises(); + + expect(openUnlockPopoutSpy).toHaveBeenCalled(); + }); + }); + + describe("fillAutofillInlineMenuCipher message handler", () => { + const pageDetails = createAutofillPageDetailsMock({ + login: { username: "username1", password: "password1" }, + }); + + it("ignores the fill request if the overlay cipher id is not provided", async () => { + sendPortMessage(listMessageConnectorSpy, { + command: "fillAutofillInlineMenuCipher", + portKey, + }); + await flushPromises(); + + expect(autofillService.isPasswordRepromptRequired).not.toHaveBeenCalled(); + expect(autofillService.doAutoFill).not.toHaveBeenCalled(); + }); + + it("ignores the fill request if the tab does not contain any identified page details", async () => { + sendPortMessage(listMessageConnectorSpy, { + command: "fillAutofillInlineMenuCipher", + inlineMenuCipherId: "inline-menu-cipher-1", + portKey, + }); + await flushPromises(); + + expect(autofillService.isPasswordRepromptRequired).not.toHaveBeenCalled(); + expect(autofillService.doAutoFill).not.toHaveBeenCalled(); + }); + + it("ignores the fill request if a master password reprompt is required", async () => { + const cipher = mock({ + reprompt: CipherRepromptType.Password, + type: CipherType.Login, + }); + overlayBackground["inlineMenuCiphers"] = new Map([["inline-menu-cipher-1", cipher]]); + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], + ]); + autofillService.isPasswordRepromptRequired.mockResolvedValue(true); + + sendPortMessage(listMessageConnectorSpy, { + command: "fillAutofillInlineMenuCipher", + inlineMenuCipherId: "inline-menu-cipher-1", + portKey, + }); + await flushPromises(); + + expect(autofillService.isPasswordRepromptRequired).toHaveBeenCalledWith(cipher, sender.tab); + expect(autofillService.doAutoFill).not.toHaveBeenCalled(); + }); + + it("autofills the selected cipher and move it to the top of the front of the ciphers map", async () => { + const cipher1 = mock({ id: "inline-menu-cipher-1" }); + const cipher2 = mock({ id: "inline-menu-cipher-2" }); + const cipher3 = mock({ id: "inline-menu-cipher-3" }); + overlayBackground["inlineMenuCiphers"] = new Map([ + ["inline-menu-cipher-1", cipher1], + ["inline-menu-cipher-2", cipher2], + ["inline-menu-cipher-3", cipher3], + ]); + 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-2", + portKey, + }); + await flushPromises(); + + expect(autofillService.isPasswordRepromptRequired).toHaveBeenCalledWith( + cipher2, + sender.tab, + ); + expect(autofillService.doAutoFill).toHaveBeenCalledWith({ + tab: sender.tab, + cipher: cipher2, + pageDetails: [pageDetailsForTab], + fillNewPassword: true, + allowTotpAutofill: true, + }); + expect(overlayBackground["inlineMenuCiphers"].entries()).toStrictEqual( + new Map([ + ["inline-menu-cipher-2", cipher2], + ["inline-menu-cipher-1", cipher1], + ["inline-menu-cipher-3", cipher3], + ]).entries(), + ); + }); + + it("copies the cipher's totp code to the clipboard after filling", async () => { + const cipher1 = mock({ id: "inline-menu-cipher-1" }); + overlayBackground["inlineMenuCiphers"] = new Map([["inline-menu-cipher-1", cipher1]]); + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], + ]); + autofillService.isPasswordRepromptRequired.mockResolvedValue(false); + const copyToClipboardSpy = jest + .spyOn(overlayBackground["platformUtilsService"], "copyToClipboard") + .mockImplementation(); + autofillService.doAutoFill.mockResolvedValue("totp-code"); + + sendPortMessage(listMessageConnectorSpy, { + command: "fillAutofillInlineMenuCipher", + inlineMenuCipherId: "inline-menu-cipher-2", + portKey, + }); + await flushPromises(); + + 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); + jest.spyOn(fido2ActiveRequestManager, "getActiveRequest"); + + sendPortMessage(listMessageConnectorSpy, { + command: "fillAutofillInlineMenuCipher", + inlineMenuCipherId: "inline-menu-cipher-1", + usePasskey: true, + portKey, + }); + await flushPromises(); + + expect(fido2ActiveRequestManager.getActiveRequest).toHaveBeenCalledWith(sender.tab.id); + }); + }); + + describe("addNewVaultItem message handler", () => { + it("skips sending the `addNewVaultItemFromOverlay` message if the sender tab does not contain the focused field", async () => { + const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + await flushPromises(); + + sendPortMessage(listMessageConnectorSpy, { command: "addNewVaultItem", portKey }); + await flushPromises(); + + expect(tabsSendMessageSpy).not.toHaveBeenCalled(); + }); + + it("sends a message to the tab to add a new vault item", async () => { + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }, sender); + await flushPromises(); + + sendPortMessage(listMessageConnectorSpy, { + command: "addNewVaultItem", + portKey, + addNewCipherType: CipherType.Login, + }); + await flushPromises(); + + expect(tabsSendMessageSpy).toHaveBeenCalledWith(sender.tab, { + command: "addNewVaultItemFromOverlay", + addNewCipherType: CipherType.Login, + }); + }); + }); + + describe("viewSelectedCipher message handler", () => { + let openViewVaultItemPopoutSpy: jest.SpyInstance; + + beforeEach(() => { + openViewVaultItemPopoutSpy = jest + .spyOn(overlayBackground as any, "openViewVaultItemPopout") + .mockImplementation(); + }); + + it("returns early if the passed cipher ID does not match one of the inline menu ciphers", async () => { + overlayBackground["inlineMenuCiphers"] = new Map([ + ["inline-menu-cipher-0", mock({ id: "inline-menu-cipher-0" })], + ]); + + sendPortMessage(listMessageConnectorSpy, { + command: "viewSelectedCipher", + inlineMenuCipherId: "inline-menu-cipher-1", + portKey, + }); + await flushPromises(); + + expect(openViewVaultItemPopoutSpy).not.toHaveBeenCalled(); + }); + + it("will open the view vault item popout with the selected cipher", async () => { + const cipher = mock({ id: "inline-menu-cipher-1" }); + overlayBackground["inlineMenuCiphers"] = new Map([ + ["inline-menu-cipher-0", mock({ id: "inline-menu-cipher-0" })], + ["inline-menu-cipher-1", cipher], + ]); + + sendPortMessage(listMessageConnectorSpy, { + command: "viewSelectedCipher", + inlineMenuCipherId: "inline-menu-cipher-1", + portKey, + }); + await flushPromises(); + + expect(openViewVaultItemPopoutSpy).toHaveBeenCalledWith(sender.tab, { + cipherId: cipher.id, + action: SHOW_AUTOFILL_BUTTON, + }); + }); + }); + + describe("redirectAutofillInlineMenuFocusOut message handler", () => { + it("redirects focus out of the inline menu list", async () => { + sendPortMessage(listMessageConnectorSpy, { + command: "redirectAutofillInlineMenuFocusOut", + direction: RedirectFocusDirection.Next, + portKey, + }); + await flushPromises(); + + expect(tabSendMessageDataSpy).toHaveBeenCalledWith( + sender.tab, + "redirectAutofillInlineMenuFocusOut", + { direction: RedirectFocusDirection.Next }, + ); + }); + }); + + describe("updateAutofillInlineMenuListHeight message handler", () => { + it("sends a message to the list port to update the menu iframe position", () => { + sendPortMessage(listMessageConnectorSpy, { + command: "updateAutofillInlineMenuListHeight", + styles: { height: "100px" }, + portKey, + }); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateAutofillInlineMenuPosition", + styles: { height: "100px" }, + }); + }); + + it("updates the inline menu position property's list height value", () => { + overlayBackground["inlineMenuPosition"] = { + list: { height: 50, top: 1, left: 2, width: 3 }, + }; + + sendPortMessage(listMessageConnectorSpy, { + command: "updateAutofillInlineMenuListHeight", + styles: { height: "100px" }, + portKey, + }); + + expect(overlayBackground["inlineMenuPosition"]).toStrictEqual({ + list: { height: 100, top: 1, left: 2, width: 3 }, + }); + }); + }); + }); + + describe("handle web navigation on committed events", () => { + describe("navigation event occurs in the top frame of the tab", () => { + it("removes the collected page details", async () => { + const sender = mock({ + tabId: 1, + frameId: 0, + }); + overlayBackground["pageDetailsForTab"][sender.tabId] = new Map([ + [sender.frameId, createPageDetailMock()], + ]); + + triggerWebNavigationOnCommittedEvent(sender); + await flushPromises(); + + expect(overlayBackground["pageDetailsForTab"][sender.tabId]).toBe(undefined); + }); + + it("clears the sub frames associated with the tab", () => { + const sender = mock({ + tabId: 1, + frameId: 0, + }); + const subFrameId = 10; + overlayBackground["subFrameOffsetsForTab"][sender.tabId] = new Map([ + [subFrameId, mock()], + ]); + + triggerWebNavigationOnCommittedEvent(sender); + + expect(overlayBackground["subFrameOffsetsForTab"][sender.tabId]).toBe(undefined); + }); + }); + + describe("navigation event occurs within sub frame", () => { + it("clears the sub frame offsets for the current frame", () => { + const sender = mock({ + tabId: 1, + frameId: 1, + }); + overlayBackground["subFrameOffsetsForTab"][sender.tabId] = new Map([ + [sender.frameId, mock()], + ]); + + triggerWebNavigationOnCommittedEvent(sender); + + expect(overlayBackground["subFrameOffsetsForTab"][sender.tabId].get(sender.frameId)).toBe( + undefined, + ); + }); + }); + }); + + describe("handle port onConnect", () => { it("skips setting up the overlay port if the port connection is not for an overlay element", async () => { const port = createPortSpyMock("not-an-overlay-element"); - await overlayBackground["handlePortOnConnect"](port); + triggerPortOnConnectEvent(port); + await flushPromises(); expect(port.onMessage.addListener).not.toHaveBeenCalled(); expect(port.postMessage).not.toHaveBeenCalled(); }); - it("sets up the overlay list port if the port connection is for the overlay list", async () => { - await initOverlayElementPorts({ initList: true, initButton: false }); + it("generates a random 12 character string used to validate port messages from the tab", async () => { + const port = createPortSpyMock(AutofillOverlayPort.Button); + overlayBackground["inlineMenuButtonPort"] = port; + + triggerPortOnConnectEvent(port); await flushPromises(); - expect(overlayBackground["overlayButtonPort"]).toBeUndefined(); - expect(listPortSpy.onMessage.addListener).toHaveBeenCalled(); - expect(listPortSpy.postMessage).toHaveBeenCalled(); - expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); - expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/list.css"); - expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); - expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith( - { overlayElement: AutofillOverlayElement.List }, - listPortSpy.sender, - ); - }); - - it("sets up the overlay button port if the port connection is for the overlay button", async () => { - await initOverlayElementPorts({ initList: false, initButton: true }); - await flushPromises(); - - expect(overlayBackground["overlayListPort"]).toBeUndefined(); - expect(buttonPortSpy.onMessage.addListener).toHaveBeenCalled(); - expect(buttonPortSpy.postMessage).toHaveBeenCalled(); - expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); - expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/button.css"); - expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); - expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith( - { overlayElement: AutofillOverlayElement.Button }, - buttonPortSpy.sender, - ); + expect(portKeyForTabSpy[port.sender.tab.id]).toHaveLength(12); }); it("stores an existing overlay port so that it can be disconnected at a later time", async () => { - overlayBackground["overlayButtonPort"] = mock(); + overlayBackground["inlineMenuButtonPort"] = mock(); await initOverlayElementPorts({ initList: false, initButton: true }); await flushPromises(); expect(overlayBackground["expiredPorts"].length).toBe(1); }); + }); - it("gets the system theme", async () => { - themeStateService.selectedTheme$ = of(ThemeType.System); + describe("handle overlay element port onMessage", () => { + let sender: chrome.runtime.MessageSender; + const portKey = "inlineMenuListPort"; - await initOverlayElementPorts({ initList: true, initButton: false }); + beforeEach(async () => { + sender = mock({ tab: { id: 1 } }); + portKeyForTabSpy[sender.tab.id] = portKey; + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + await initOverlayElementPorts(); + listMessageConnectorSpy.sender = sender; + openUnlockPopoutSpy.mockImplementation(); + }); + + it("ignores messages that do not contain a valid portKey", async () => { + triggerPortOnMessageEvent(buttonMessageConnectorSpy, { + command: "autofillInlineMenuBlurred", + }); await flushPromises(); - expect(listPortSpy.postMessage).toHaveBeenCalledWith( - expect.objectContaining({ theme: ThemeType.System }), - ); + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuListFocused", + }); + }); + + it("ignores messages from ports that are not listened to", () => { + triggerPortOnMessageEvent(buttonPortSpy, { + command: "autofillInlineMenuBlurred", + portKey, + }); + + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillInlineMenuListFocused", + }); }); }); - describe("handleOverlayElementPortMessage", () => { - beforeEach(async () => { + describe("handle port onDisconnect", () => { + it("sets the disconnected port to a `null` value", async () => { await initOverlayElementPorts(); - overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; - }); - it("ignores port messages that do not contain a handler", () => { - jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); + triggerPortOnDisconnectEvent(buttonPortSpy); + triggerPortOnDisconnectEvent(listPortSpy); + await flushPromises(); - sendPortMessage(buttonPortSpy, { command: "checkAutofillOverlayButtonFocused" }); - - expect(overlayBackground["checkOverlayButtonFocused"]).not.toHaveBeenCalled(); - }); - - describe("overlay button message handlers", () => { - it("unlocks the vault if the user auth status is not unlocked", () => { - overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; - jest.spyOn(overlayBackground as any, "unlockVault").mockImplementation(); - - sendPortMessage(buttonPortSpy, { command: "overlayButtonClicked" }); - - expect(overlayBackground["unlockVault"]).toHaveBeenCalled(); - }); - - it("opens the autofill overlay if the auth status is unlocked", () => { - jest.spyOn(overlayBackground as any, "openOverlay").mockImplementation(); - - sendPortMessage(buttonPortSpy, { command: "overlayButtonClicked" }); - - expect(overlayBackground["openOverlay"]).toHaveBeenCalled(); - }); - - describe("closeAutofillOverlay", () => { - it("sends a `closeOverlay` message to the sender tab", () => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - - sendPortMessage(buttonPortSpy, { command: "closeAutofillOverlay" }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - buttonPortSpy.sender.tab, - "closeAutofillOverlay", - { forceCloseOverlay: false }, - ); - }); - }); - - describe("forceCloseAutofillOverlay", () => { - it("sends a `closeOverlay` message to the sender tab with a `forceCloseOverlay` flag of `true` set", () => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - - sendPortMessage(buttonPortSpy, { command: "forceCloseAutofillOverlay" }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - buttonPortSpy.sender.tab, - "closeAutofillOverlay", - { forceCloseOverlay: true }, - ); - }); - }); - - describe("overlayPageBlurred", () => { - it("checks if the overlay list is focused", () => { - jest.spyOn(overlayBackground as any, "checkOverlayListFocused"); - - sendPortMessage(buttonPortSpy, { command: "overlayPageBlurred" }); - - expect(overlayBackground["checkOverlayListFocused"]).toHaveBeenCalled(); - }); - }); - - describe("redirectOverlayFocusOut", () => { - beforeEach(() => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - }); - - it("ignores the redirect message if the direction is not provided", () => { - sendPortMessage(buttonPortSpy, { command: "redirectOverlayFocusOut" }); - - expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled(); - }); - - it("sends the redirect message if the direction is provided", () => { - sendPortMessage(buttonPortSpy, { - command: "redirectOverlayFocusOut", - direction: RedirectFocusDirection.Next, - }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - buttonPortSpy.sender.tab, - "redirectOverlayFocusOut", - { direction: RedirectFocusDirection.Next }, - ); - }); - }); - }); - - describe("overlay list message handlers", () => { - describe("checkAutofillOverlayButtonFocused", () => { - it("checks on the focus state of the overlay button", () => { - jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); - - sendPortMessage(listPortSpy, { command: "checkAutofillOverlayButtonFocused" }); - - expect(overlayBackground["checkOverlayButtonFocused"]).toHaveBeenCalled(); - }); - }); - - describe("forceCloseAutofillOverlay", () => { - it("sends a `closeOverlay` message to the sender tab with a `forceCloseOverlay` flag of `true` set", () => { - jest.spyOn(BrowserApi, "tabSendMessageData"); - - sendPortMessage(listPortSpy, { command: "forceCloseAutofillOverlay" }); - - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - listPortSpy.sender.tab, - "closeAutofillOverlay", - { forceCloseOverlay: true }, - ); - }); - }); - - describe("overlayPageBlurred", () => { - it("checks on the focus state of the overlay button", () => { - jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); - - sendPortMessage(listPortSpy, { command: "overlayPageBlurred" }); - - expect(overlayBackground["checkOverlayButtonFocused"]).toHaveBeenCalled(); - }); - }); - - describe("unlockVault", () => { - it("closes the autofill overlay and opens the unlock popout", async () => { - jest.spyOn(overlayBackground as any, "closeOverlay").mockImplementation(); - jest.spyOn(overlayBackground as any, "openUnlockPopout").mockImplementation(); - jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); - - sendPortMessage(listPortSpy, { command: "unlockVault" }); - await flushPromises(); - - expect(overlayBackground["closeOverlay"]).toHaveBeenCalledWith(listPortSpy); - expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( - listPortSpy.sender.tab, - "addToLockedVaultPendingNotifications", - { - commandToRetry: { - message: { command: "openAutofillOverlay" }, - sender: listPortSpy.sender, - }, - target: "overlay.background", - }, - ); - expect(overlayBackground["openUnlockPopout"]).toHaveBeenCalledWith( - listPortSpy.sender.tab, - true, - ); - }); - }); - - describe("fillSelectedListItem", () => { - let getLoginCiphersSpy: jest.SpyInstance; - let isPasswordRepromptRequiredSpy: jest.SpyInstance; - let doAutoFillSpy: jest.SpyInstance; - let sender: chrome.runtime.MessageSender; - const pageDetails = createAutofillPageDetailsMock({ - login: { username: "username1", password: "password1" }, - }); - - beforeEach(() => { - getLoginCiphersSpy = jest.spyOn(overlayBackground["overlayLoginCiphers"], "get"); - isPasswordRepromptRequiredSpy = jest.spyOn( - overlayBackground["autofillService"], - "isPasswordRepromptRequired", - ); - doAutoFillSpy = jest.spyOn(overlayBackground["autofillService"], "doAutoFill"); - sender = mock({ tab: { id: 1 } }); - }); - - it("ignores the fill request if the overlay cipher id is not provided", async () => { - sendPortMessage(listPortSpy, { command: "fillSelectedListItem" }); - await flushPromises(); - - expect(getLoginCiphersSpy).not.toHaveBeenCalled(); - expect(isPasswordRepromptRequiredSpy).not.toHaveBeenCalled(); - expect(doAutoFillSpy).not.toHaveBeenCalled(); - }); - - it("ignores the fill request if the tab does not contain any identified page details", async () => { - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(getLoginCiphersSpy).not.toHaveBeenCalled(); - expect(isPasswordRepromptRequiredSpy).not.toHaveBeenCalled(); - expect(doAutoFillSpy).not.toHaveBeenCalled(); - }); - - it("ignores the fill request if a master password reprompt is required", async () => { - const cipher = mock({ - reprompt: CipherRepromptType.Password, - type: CipherType.Login, - }); - overlayBackground["overlayLoginCiphers"] = new Map([["overlay-cipher-1", cipher]]); - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], - ]); - getLoginCiphersSpy = jest.spyOn(overlayBackground["overlayLoginCiphers"], "get"); - isPasswordRepromptRequiredSpy.mockResolvedValue(true); - - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(getLoginCiphersSpy).toHaveBeenCalled(); - expect(isPasswordRepromptRequiredSpy).toHaveBeenCalledWith( - cipher, - listPortSpy.sender.tab, - ); - expect(doAutoFillSpy).not.toHaveBeenCalled(); - }); - - it("auto-fills the selected cipher and move it to the top of the front of the ciphers map", async () => { - const cipher1 = mock({ id: "overlay-cipher-1" }); - const cipher2 = mock({ id: "overlay-cipher-2" }); - const cipher3 = mock({ id: "overlay-cipher-3" }); - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-1", cipher1], - ["overlay-cipher-2", cipher2], - ["overlay-cipher-3", cipher3], - ]); - const pageDetailsForTab = { - frameId: sender.frameId, - tab: sender.tab, - details: pageDetails, - }; - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, pageDetailsForTab], - ]); - isPasswordRepromptRequiredSpy.mockResolvedValue(false); - - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-2", - }); - await flushPromises(); - - expect(isPasswordRepromptRequiredSpy).toHaveBeenCalledWith( - cipher2, - listPortSpy.sender.tab, - ); - expect(doAutoFillSpy).toHaveBeenCalledWith({ - tab: listPortSpy.sender.tab, - cipher: cipher2, - pageDetails: [pageDetailsForTab], - fillNewPassword: true, - allowTotpAutofill: true, - }); - expect(overlayBackground["overlayLoginCiphers"].entries()).toStrictEqual( - new Map([ - ["overlay-cipher-2", cipher2], - ["overlay-cipher-1", cipher1], - ["overlay-cipher-3", cipher3], - ]).entries(), - ); - }); - - it("copies the cipher's totp code to the clipboard after filling", async () => { - const cipher1 = mock({ id: "overlay-cipher-1" }); - overlayBackground["overlayLoginCiphers"] = new Map([["overlay-cipher-1", cipher1]]); - overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ - [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], - ]); - isPasswordRepromptRequiredSpy.mockResolvedValue(false); - const copyToClipboardSpy = jest - .spyOn(overlayBackground["platformUtilsService"], "copyToClipboard") - .mockImplementation(); - doAutoFillSpy.mockReturnValueOnce("totp-code"); - - sendPortMessage(listPortSpy, { - command: "fillSelectedListItem", - overlayCipherId: "overlay-cipher-2", - }); - await flushPromises(); - - expect(copyToClipboardSpy).toHaveBeenCalledWith("totp-code"); - }); - }); - - describe("getNewVaultItemDetails", () => { - it("will send an addNewVaultItemFromOverlay message", async () => { - jest.spyOn(BrowserApi, "tabSendMessage"); - - sendPortMessage(listPortSpy, { command: "addNewVaultItem" }); - await flushPromises(); - - expect(BrowserApi.tabSendMessage).toHaveBeenCalledWith(listPortSpy.sender.tab, { - command: "addNewVaultItemFromOverlay", - }); - }); - }); - - describe("viewSelectedCipher", () => { - let openViewVaultItemPopoutSpy: jest.SpyInstance; - - beforeEach(() => { - openViewVaultItemPopoutSpy = jest - .spyOn(overlayBackground as any, "openViewVaultItemPopout") - .mockImplementation(); - }); - - it("returns early if the passed cipher ID does not match one of the overlay login ciphers", async () => { - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-0", mock({ id: "overlay-cipher-0" })], - ]); - - sendPortMessage(listPortSpy, { - command: "viewSelectedCipher", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(openViewVaultItemPopoutSpy).not.toHaveBeenCalled(); - }); - - it("will open the view vault item popout with the selected cipher", async () => { - const cipher = mock({ id: "overlay-cipher-1" }); - overlayBackground["overlayLoginCiphers"] = new Map([ - ["overlay-cipher-0", mock({ id: "overlay-cipher-0" })], - ["overlay-cipher-1", cipher], - ]); - - sendPortMessage(listPortSpy, { - command: "viewSelectedCipher", - overlayCipherId: "overlay-cipher-1", - }); - await flushPromises(); - - expect(overlayBackground["openViewVaultItemPopout"]).toHaveBeenCalledWith( - listPortSpy.sender.tab, - { - cipherId: cipher.id, - action: SHOW_AUTOFILL_BUTTON, - }, - ); - }); - }); - - describe("redirectOverlayFocusOut", () => { - it("redirects focus out of the overlay list", async () => { - const message = { - command: "redirectOverlayFocusOut", - direction: RedirectFocusDirection.Next, - }; - const redirectOverlayFocusOutSpy = jest.spyOn( - overlayBackground as any, - "redirectOverlayFocusOut", - ); - - sendPortMessage(listPortSpy, message); - await flushPromises(); - - expect(redirectOverlayFocusOutSpy).toHaveBeenCalledWith(message, listPortSpy); - }); - }); + expect(overlayBackground["inlineMenuListPort"]).toBeNull(); + expect(overlayBackground["inlineMenuButtonPort"]).toBeNull(); }); }); }); diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index bf954c3419f..0047d1de28e 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -1,112 +1,248 @@ -import { firstValueFrom } from "rxjs"; +import { + firstValueFrom, + merge, + ReplaySubject, + Subject, + throttleTime, + switchMap, + debounceTime, + Observable, + map, +} from "rxjs"; +import { parse } from "tldts"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { SHOW_AUTOFILL_BUTTON } from "@bitwarden/common/autofill/constants"; +import { + AutofillOverlayVisibility, + SHOW_AUTOFILL_BUTTON, +} from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; +import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { + Fido2ActiveRequestEvents, + Fido2ActiveRequestManager, +} from "@bitwarden/common/platform/abstractions/fido2/fido2-active-request-manager.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"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; 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"; import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window"; import { BrowserApi } from "../../platform/browser/browser-api"; import { - openViewVaultItemPopout, openAddEditVaultItemPopout, + openViewVaultItemPopout, } from "../../vault/popup/utils/vault-popout-window"; -import { AutofillService, PageDetail } from "../services/abstractions/autofill.service"; -import { AutofillOverlayElement, AutofillOverlayPort } from "../utils/autofill-overlay.enum"; +import { + AutofillOverlayElement, + AutofillOverlayPort, + MAX_SUB_FRAME_DEPTH, +} from "../enums/autofill-overlay.enum"; +import { AutofillService } from "../services/abstractions/autofill.service"; +import { generateRandomChars } from "../utils"; import { LockedVaultPendingNotificationsData } from "./abstractions/notification.background"; import { + BuildCipherDataParams, + CloseInlineMenuMessage, + CurrentAddNewItemData, FocusedFieldData, - OverlayBackgroundExtensionMessageHandlers, - OverlayButtonPortMessageHandlers, - OverlayCipherData, - OverlayListPortMessageHandlers, + InlineMenuButtonPortMessageHandlers, + InlineMenuCipherData, + InlineMenuListPortMessageHandlers, + InlineMenuPosition, + NewCardCipherData, + NewIdentityCipherData, + NewLoginCipherData, + OverlayAddNewItemMessage, OverlayBackground as OverlayBackgroundInterface, OverlayBackgroundExtensionMessage, - OverlayAddNewItemMessage, + OverlayBackgroundExtensionMessageHandlers, OverlayPortMessage, - WebsiteIconData, + PageDetailsForTab, + SubFrameOffsetData, + SubFrameOffsetsForTab, + ToggleInlineMenuHiddenMessage, } from "./abstractions/overlay.background"; -class OverlayBackground implements OverlayBackgroundInterface { +export class OverlayBackground implements OverlayBackgroundInterface { private readonly openUnlockPopout = openUnlockPopout; private readonly openViewVaultItemPopout = openViewVaultItemPopout; private readonly openAddEditVaultItemPopout = openAddEditVaultItemPopout; - private overlayLoginCiphers: Map = new Map(); - private pageDetailsForTab: Record< - chrome.runtime.MessageSender["tab"]["id"], - Map - > = {}; - private userAuthStatus: AuthenticationStatus = AuthenticationStatus.LoggedOut; - private overlayButtonPort: chrome.runtime.Port; - private overlayListPort: chrome.runtime.Port; + private readonly storeInlineMenuFido2CredentialsSubject = new ReplaySubject(1); + private pageDetailsForTab: PageDetailsForTab = {}; + private subFrameOffsetsForTab: SubFrameOffsetsForTab = {}; + private portKeyForTab: Record = {}; private expiredPorts: chrome.runtime.Port[] = []; + 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; + private currentInlineMenuCiphersCount: number = 0; + private delayedCloseTimeout: number | NodeJS.Timeout; + private startInlineMenuFadeInSubject = new Subject(); + private cancelInlineMenuFadeInSubject = new Subject(); + private startUpdateInlineMenuPositionSubject = new Subject(); + private cancelUpdateInlineMenuPositionSubject = new Subject(); + private repositionInlineMenuSubject = new Subject(); + private rebuildSubFrameOffsetsSubject = new Subject(); + private addNewVaultItemSubject = new Subject(); + private currentAddNewItemData: CurrentAddNewItemData; private focusedFieldData: FocusedFieldData; - private overlayPageTranslations: Record; + private isFieldCurrentlyFocused: boolean = false; + private isFieldCurrentlyFilling: boolean = false; + private isInlineMenuButtonVisible: boolean = false; + private isInlineMenuListVisible: boolean = false; + private showPasskeysLabelsWithinInlineMenu: boolean = false; private iconsServerUrl: string; private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = { - openAutofillOverlay: () => this.openOverlay(false), autofillOverlayElementClosed: ({ message, sender }) => this.overlayElementClosed(message, sender), autofillOverlayAddNewVaultItem: ({ message, sender }) => this.addNewVaultItem(message, sender), - getAutofillOverlayVisibility: () => this.getOverlayVisibility(), - checkAutofillOverlayFocused: () => this.checkOverlayFocused(), - focusAutofillOverlayList: () => this.focusOverlayList(), - updateAutofillOverlayPosition: ({ message, sender }) => - this.updateOverlayPosition(message, sender), - updateAutofillOverlayHidden: ({ message }) => this.updateOverlayHidden(message), + triggerAutofillOverlayReposition: ({ sender }) => this.triggerOverlayReposition(sender), + checkIsInlineMenuCiphersPopulated: ({ sender }) => + this.checkIsInlineMenuCiphersPopulated(sender), updateFocusedFieldData: ({ message, sender }) => this.setFocusedFieldData(message, sender), + updateIsFieldCurrentlyFocused: ({ message, sender }) => + this.updateIsFieldCurrentlyFocused(message, sender), + checkIsFieldCurrentlyFocused: () => this.checkIsFieldCurrentlyFocused(), + updateIsFieldCurrentlyFilling: ({ message }) => this.updateIsFieldCurrentlyFilling(message), + checkIsFieldCurrentlyFilling: () => this.checkIsFieldCurrentlyFilling(), + getAutofillInlineMenuVisibility: () => this.getInlineMenuVisibility(), + openAutofillInlineMenu: () => this.openInlineMenu(false), + closeAutofillInlineMenu: ({ message, sender }) => this.closeInlineMenu(sender, message), + checkAutofillInlineMenuFocused: ({ sender }) => this.checkInlineMenuFocused(sender), + focusAutofillInlineMenuList: () => this.focusInlineMenuList(), + updateAutofillInlineMenuPosition: ({ message, sender }) => + this.updateInlineMenuPosition(message, sender), + getAutofillInlineMenuPosition: () => this.getInlineMenuPosition(), + updateAutofillInlineMenuElementIsVisibleStatus: ({ message, sender }) => + this.updateInlineMenuElementIsVisibleStatus(message, sender), + checkIsAutofillInlineMenuButtonVisible: () => this.checkIsInlineMenuButtonVisible(), + checkIsAutofillInlineMenuListVisible: () => this.checkIsInlineMenuListVisible(), + getCurrentTabFrameId: ({ sender }) => this.getSenderFrameId(sender), + updateSubFrameData: ({ message, sender }) => this.updateSubFrameData(message, sender), + triggerSubFrameFocusInRebuild: ({ sender }) => this.triggerSubFrameFocusInRebuild(sender), + destroyAutofillInlineMenuListeners: ({ message, sender }) => + this.triggerDestroyInlineMenuListeners(sender.tab, message.subFrameData.frameId), collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender), unlockCompleted: ({ message }) => this.unlockCompleted(message), + doFullSync: () => this.updateOverlayCiphers(), addedCipher: () => this.updateOverlayCiphers(), addEditCipherSubmitted: () => this.updateOverlayCiphers(), editedCipher: () => this.updateOverlayCiphers(), deletedCipher: () => this.updateOverlayCiphers(), + fido2AbortRequest: ({ sender }) => this.abortFido2ActiveRequest(sender), }; - private readonly overlayButtonPortMessageHandlers: OverlayButtonPortMessageHandlers = { - overlayButtonClicked: ({ port }) => this.handleOverlayButtonClicked(port), - closeAutofillOverlay: ({ port }) => this.closeOverlay(port), - forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true), - overlayPageBlurred: () => this.checkOverlayListFocused(), - redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port), + private readonly inlineMenuButtonPortMessageHandlers: InlineMenuButtonPortMessageHandlers = { + triggerDelayedAutofillInlineMenuClosure: () => this.triggerDelayedInlineMenuClosure(), + autofillInlineMenuButtonClicked: ({ port }) => this.handleInlineMenuButtonClicked(port), + autofillInlineMenuBlurred: () => this.checkInlineMenuListFocused(), + redirectAutofillInlineMenuFocusOut: ({ message, port }) => + this.redirectInlineMenuFocusOut(message, port), + updateAutofillInlineMenuColorScheme: () => this.updateInlineMenuButtonColorScheme(), }; - private readonly overlayListPortMessageHandlers: OverlayListPortMessageHandlers = { - checkAutofillOverlayButtonFocused: () => this.checkOverlayButtonFocused(), - forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true), - overlayPageBlurred: () => this.checkOverlayButtonFocused(), + private readonly inlineMenuListPortMessageHandlers: InlineMenuListPortMessageHandlers = { + checkAutofillInlineMenuButtonFocused: () => this.checkInlineMenuButtonFocused(), + autofillInlineMenuBlurred: () => this.checkInlineMenuButtonFocused(), unlockVault: ({ port }) => this.unlockVault(port), - fillSelectedListItem: ({ message, port }) => this.fillSelectedOverlayListItem(message, port), - addNewVaultItem: ({ port }) => this.getNewVaultItemDetails(port), + fillAutofillInlineMenuCipher: ({ message, port }) => this.fillInlineMenuCipher(message, port), + addNewVaultItem: ({ message, port }) => this.getNewVaultItemDetails(message, port), viewSelectedCipher: ({ message, port }) => this.viewSelectedCipher(message, port), - redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port), + redirectAutofillInlineMenuFocusOut: ({ message, port }) => + this.redirectInlineMenuFocusOut(message, port), + updateAutofillInlineMenuListHeight: ({ message }) => this.updateInlineMenuListHeight(message), }; constructor( + private logService: LogService, private cipherService: CipherService, private autofillService: AutofillService, private authService: AuthService, private environmentService: EnvironmentService, private domainSettingsService: DomainSettingsService, - private stateService: StateService, private autofillSettingsService: AutofillSettingsServiceAbstraction, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, + private vaultSettingsService: VaultSettingsService, + private fido2ActiveRequestManager: Fido2ActiveRequestManager, private themeStateService: ThemeStateService, - ) {} + ) { + this.initOverlayEventObservables(); + } + + /** + * Sets up the extension message listeners and gets the settings for the + * overlay's visibility and the user's authentication status. + */ + async init() { + this.setupExtensionListeners(); + const env = await firstValueFrom(this.environmentService.environment$); + this.iconsServerUrl = env.getIconsUrl(); + } + + /** + * Initializes event observables that handle events which affect the overlay's behavior. + */ + private initOverlayEventObservables() { + this.storeInlineMenuFido2CredentialsSubject + .pipe(switchMap((tabId) => this.availablePasskeyAuthCredentials$(tabId))) + .subscribe((credentials) => this.storeInlineMenuFido2Credentials(credentials)); + this.repositionInlineMenuSubject + .pipe( + debounceTime(1000), + switchMap((sender) => this.repositionInlineMenu(sender)), + ) + .subscribe(); + this.rebuildSubFrameOffsetsSubject + .pipe( + throttleTime(100), + switchMap((sender) => this.rebuildSubFrameOffsets(sender)), + ) + .subscribe(); + this.addNewVaultItemSubject + .pipe( + debounceTime(100), + switchMap((addNewItemData) => + this.buildCipherAndOpenAddEditVaultItemPopout(addNewItemData), + ), + ) + .subscribe(); + + // Debounce used to update inline menu position + merge( + this.startUpdateInlineMenuPositionSubject.pipe(debounceTime(150)), + this.cancelUpdateInlineMenuPositionSubject, + ) + .pipe(switchMap((sender) => this.updateInlineMenuPositionAfterRepositionEvent(sender))) + .subscribe(); + + // FadeIn Observable behavior + merge( + this.startInlineMenuFadeInSubject.pipe(debounceTime(150)), + this.cancelInlineMenuFadeInSubject, + ) + .pipe(switchMap((cancelSignal) => this.triggerInlineMenuFadeIn(!!cancelSignal))) + .subscribe(); + } /** * Removes cached page details for a tab @@ -115,89 +251,446 @@ class OverlayBackground implements OverlayBackgroundInterface { * @param tabId - Used to reference the page details of a specific tab */ removePageDetails(tabId: number) { - if (!this.pageDetailsForTab[tabId]) { - return; + if (this.pageDetailsForTab[tabId]) { + this.pageDetailsForTab[tabId].clear(); + delete this.pageDetailsForTab[tabId]; } - this.pageDetailsForTab[tabId].clear(); - delete this.pageDetailsForTab[tabId]; + if (this.portKeyForTab[tabId]) { + delete this.portKeyForTab[tabId]; + } } /** - * Sets up the extension message listeners and gets the settings for the - * overlay's visibility and the user's authentication status. - */ - async init() { - this.setupExtensionMessageListeners(); - const env = await firstValueFrom(this.environmentService.environment$); - this.iconsServerUrl = env.getIconsUrl(); - await this.getOverlayVisibility(); - await this.getAuthStatus(); - } - - /** - * Updates the overlay list's ciphers and sends the updated list to the overlay list iframe. + * Updates the inline menu list's ciphers and sends the updated list to the inline menu list iframe. * Queries all ciphers for the given url, and sorts them by last used. Will not update the * list of ciphers if the extension is not unlocked. */ - async updateOverlayCiphers() { + async updateOverlayCiphers(updateAllCipherTypes = true) { const authStatus = await firstValueFrom(this.authService.activeAccountStatus$); if (authStatus !== AuthenticationStatus.Unlocked) { + if (this.focusedFieldData) { + this.closeInlineMenuAfterCiphersUpdate().catch((error) => this.logService.error(error)); + } return; } const currentTab = await BrowserApi.getTabFromCurrentWindowId(); - if (!currentTab?.url) { + if (this.focusedFieldData && currentTab?.id !== this.focusedFieldData.tabId) { + this.closeInlineMenuAfterCiphersUpdate().catch((error) => this.logService.error(error)); + } + + if (!currentTab || !currentTab.url?.startsWith("http")) { + if (updateAllCipherTypes) { + this.cardAndIdentityCiphers = null; + } return; } - this.overlayLoginCiphers = new Map(); - const ciphersViews = (await this.cipherService.getAllDecryptedForUrl(currentTab.url)).sort( - (a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b), - ); - for (let cipherIndex = 0; cipherIndex < ciphersViews.length; cipherIndex++) { - this.overlayLoginCiphers.set(`overlay-cipher-${cipherIndex}`, ciphersViews[cipherIndex]); + const request = this.fido2ActiveRequestManager.getActiveRequest(currentTab.id); + if (request) { + request.subject.next({ type: Fido2ActiveRequestEvents.Refresh }); } - const ciphers = await this.getOverlayCipherData(); - this.overlayListPort?.postMessage({ command: "updateOverlayListCiphers", ciphers }); - await BrowserApi.tabSendMessageData(currentTab, "updateIsOverlayCiphersPopulated", { - isOverlayCiphersPopulated: Boolean(ciphers.length), + 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++) { + this.inlineMenuCiphers.set(`inline-menu-cipher-${cipherIndex}`, ciphersViews[cipherIndex]); + } + + const ciphers = await this.getInlineMenuCipherData(); + this.inlineMenuListPort?.postMessage({ + command: "updateAutofillInlineMenuListCiphers", + ciphers, + showInlineMenuAccountCreation: this.showInlineMenuAccountCreation(), + showPasskeysLabels: this.showPasskeysLabelsWithinInlineMenu, }); } /** - * Strips out unnecessary data from the ciphers and returns an array of - * objects that contain the cipher data needed for the overlay list. + * Gets the decrypted ciphers within a user's vault based on the current tab's URL. + * + * @param currentTab - The current tab + * @param updateAllCipherTypes - Identifies credit card and identity cipher types should also be updated */ - private async getOverlayCipherData(): Promise { - const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$); - const overlayCiphersArray = Array.from(this.overlayLoginCiphers); - const overlayCipherData: OverlayCipherData[] = []; - let loginCipherIcon: WebsiteIconData; - - for (let cipherIndex = 0; cipherIndex < overlayCiphersArray.length; cipherIndex++) { - const [overlayCipherId, cipher] = overlayCiphersArray[cipherIndex]; - if (!loginCipherIcon && cipher.type === CipherType.Login) { - loginCipherIcon = buildCipherIcon(this.iconsServerUrl, cipher, showFavicons); - } - - overlayCipherData.push({ - id: overlayCipherId, - name: cipher.name, - type: cipher.type, - reprompt: cipher.reprompt, - favorite: cipher.favorite, - icon: - cipher.type === CipherType.Login - ? loginCipherIcon - : buildCipherIcon(this.iconsServerUrl, cipher, showFavicons), - login: cipher.type === CipherType.Login ? { username: cipher.login.username } : null, - card: cipher.type === CipherType.Card ? cipher.card.subTitle : null, - }); + private async getCipherViews( + currentTab: chrome.tabs.Tab, + updateAllCipherTypes: boolean, + ): Promise { + if (updateAllCipherTypes || !this.cardAndIdentityCiphers) { + return this.getAllCipherTypeViews(currentTab); } - return overlayCipherData; + const cipherViews = (await this.cipherService.getAllDecryptedForUrl(currentTab.url || "")).sort( + (a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b), + ); + + return this.cardAndIdentityCiphers + ? cipherViews.concat(...this.cardAndIdentityCiphers) + : cipherViews; + } + + /** + * Queries all cipher types from the user's vault returns them sorted by last used. + * + * @param currentTab - The current tab + */ + private async getAllCipherTypeViews(currentTab: chrome.tabs.Tab): Promise { + if (!this.cardAndIdentityCiphers) { + this.cardAndIdentityCiphers = new Set([]); + } + + this.cardAndIdentityCiphers.clear(); + const cipherViews = ( + await this.cipherService.getAllDecryptedForUrl(currentTab.url || "", [ + CipherType.Card, + CipherType.Identity, + ]) + ).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b)); + for (let cipherIndex = 0; cipherIndex < cipherViews.length; cipherIndex++) { + const cipherView = cipherViews[cipherIndex]; + if ( + !this.cardAndIdentityCiphers.has(cipherView) && + [CipherType.Card, CipherType.Identity].includes(cipherView.type) + ) { + this.cardAndIdentityCiphers.add(cipherView); + } + } + + if (!this.cardAndIdentityCiphers.size) { + this.cardAndIdentityCiphers = null; + } + + return cipherViews; + } + + /** + * Strips out unnecessary data from the ciphers and returns an array of + * objects that contain the cipher data needed for the inline menu list. + */ + private async getInlineMenuCipherData(): Promise { + 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( + inlineMenuCiphersArray, + true, + ); + } else { + inlineMenuCipherData = await this.buildInlineMenuCiphers( + inlineMenuCiphersArray, + showFavicons, + ); + } + + this.currentInlineMenuCiphersCount = inlineMenuCipherData.length; + return inlineMenuCipherData; + } + + /** + * Builds the inline menu ciphers for a form field that is meant for account creation. + * + * @param inlineMenuCiphersArray - Array of inline menu ciphers + * @param showFavicons - Identifies whether favicons should be shown + */ + private buildInlineMenuAccountCreationCiphers( + inlineMenuCiphersArray: [string, CipherView][], + showFavicons: boolean, + ) { + const inlineMenuCipherData: InlineMenuCipherData[] = []; + const accountCreationLoginCiphers: InlineMenuCipherData[] = []; + + for (let cipherIndex = 0; cipherIndex < inlineMenuCiphersArray.length; cipherIndex++) { + const [inlineMenuCipherId, cipher] = inlineMenuCiphersArray[cipherIndex]; + + if (cipher.type === CipherType.Login) { + accountCreationLoginCiphers.push( + this.buildCipherData({ + inlineMenuCipherId, + cipher, + showFavicons, + showInlineMenuAccountCreation: true, + }), + ); + continue; + } + + if (cipher.type !== CipherType.Identity || !this.focusedFieldData?.accountCreationFieldType) { + continue; + } + + const identity = this.getIdentityCipherData(cipher, true); + if (!identity?.username) { + continue; + } + + inlineMenuCipherData.push( + this.buildCipherData({ + inlineMenuCipherId, + cipher, + showFavicons, + showInlineMenuAccountCreation: true, + identityData: identity, + }), + ); + } + + if (accountCreationLoginCiphers.length) { + return inlineMenuCipherData.concat(accountCreationLoginCiphers); + } + + return inlineMenuCipherData; + } + + /** + * Builds the inline menu ciphers for a form field that is not meant for account creation. + * + * @param inlineMenuCiphersArray - Array of inline menu ciphers + * @param showFavicons - Identifies whether favicons should be shown + */ + 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())); + } + const passkeysEnabled = await firstValueFrom(this.vaultSettingsService.enablePasskeys$); + + for (let cipherIndex = 0; cipherIndex < inlineMenuCiphersArray.length; cipherIndex++) { + const [inlineMenuCipherId, cipher] = inlineMenuCiphersArray[cipherIndex]; + if (this.focusedFieldData?.filledByCipherType !== cipher.type) { + continue; + } + + if (!passkeysEnabled || !(await this.showCipherAsPasskey(cipher, domainExclusionsSet))) { + inlineMenuCipherData.push( + this.buildCipherData({ inlineMenuCipherId, cipher, showFavicons }), + ); + continue; + } + + passkeyCipherData.push( + this.buildCipherData({ + inlineMenuCipherId, + cipher, + showFavicons, + hasPasskey: true, + }), + ); + + if (cipher.login?.password && cipher.login.username) { + 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 async showCipherAsPasskey( + cipher: CipherView, + domainExclusions: Set | null, + ): Promise { + if (cipher.type !== CipherType.Login || !this.focusedFieldData?.showPasskeys) { + 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.has(credentialId); + } + + /** + * Builds the cipher data for the inline menu list. + * + * @param inlineMenuCipherId - The ID of the inline menu cipher + * @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, + cipher, + showFavicons, + showInlineMenuAccountCreation, + hasPasskey, + identityData, + }: BuildCipherDataParams): InlineMenuCipherData { + const inlineMenuData: InlineMenuCipherData = { + id: inlineMenuCipherId, + name: cipher.name, + type: cipher.type, + reprompt: cipher.reprompt, + favorite: cipher.favorite, + icon: buildCipherIcon(this.iconsServerUrl, cipher, showFavicons), + accountCreationFieldType: this.focusedFieldData?.accountCreationFieldType, + }; + + if (cipher.type === CipherType.Login) { + inlineMenuData.login = { + username: cipher.login.username, + passkey: hasPasskey + ? { + rpName: cipher.login.fido2Credentials[0].rpName, + userName: cipher.login.fido2Credentials[0].userName, + } + : null, + }; + return inlineMenuData; + } + + if (cipher.type === CipherType.Card) { + inlineMenuData.card = cipher.card.subTitle; + return inlineMenuData; + } + + inlineMenuData.identity = + identityData || this.getIdentityCipherData(cipher, showInlineMenuAccountCreation); + return inlineMenuData; + } + + /** + * Gets the identity data for a cipher based on whether the inline menu is for account creation. + * + * @param cipher - The cipher to get the identity data for + * @param showInlineMenuAccountCreation - Identifies whether the inline menu is for account creation + */ + private getIdentityCipherData( + cipher: CipherView, + showInlineMenuAccountCreation: boolean = false, + ): { fullName: string; username?: string } { + const { firstName, lastName } = cipher.identity; + + let fullName = ""; + if (firstName) { + fullName += firstName; + } + + if (lastName) { + fullName += ` ${lastName}`; + fullName = fullName.trim(); + } + + if ( + !showInlineMenuAccountCreation || + !this.focusedFieldData?.accountCreationFieldType || + this.focusedFieldData.accountCreationFieldType === "password" + ) { + return { fullName }; + } + + return { + fullName, + username: + this.focusedFieldData.accountCreationFieldType === "email" + ? cipher.identity.email + : cipher.identity.username, + }; + } + + /** + * Identifies whether the inline menu is being shown on an account creation field. + */ + private showInlineMenuAccountCreation(): boolean { + if (typeof this.focusedFieldData?.showInlineMenuAccountCreation !== "undefined") { + return this.focusedFieldData?.showInlineMenuAccountCreation; + } + + if (this.focusedFieldData?.filledByCipherType !== CipherType.Login) { + return false; + } + + if (this.cardAndIdentityCiphers) { + return this.inlineMenuCiphers.size === this.cardAndIdentityCiphers.size; + } + + 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[]) { + this.inlineMenuFido2Credentials.clear(); + + credentials.forEach( + (credential) => + credential?.credentialId && this.inlineMenuFido2Credentials.add(credential.credentialId), + ); + } + + /** + * Gets the passkey credentials available from an active FIDO2 request for a given tab. + * + * @param tabId - The tab id to get the active request for. + */ + private availablePasskeyAuthCredentials$(tabId: number): Observable { + return this.fido2ActiveRequestManager + .getActiveRequest$(tabId) + .pipe(map((request) => request?.credentials ?? [])); + } + + /** + * Aborts an active FIDO2 request for a given tab and updates the inline menu ciphers. + * + * @param sender - The sender of the message + */ + private async abortFido2ActiveRequest(sender: chrome.runtime.MessageSender) { + this.fido2ActiveRequestManager.removeActiveRequest(sender.tab.id); + await this.updateOverlayCiphers(false); + } + + /** + * 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. + */ + private async closeInlineMenuAfterCiphersUpdate() { + const focusedFieldTab = await BrowserApi.getTab(this.focusedFieldData.tabId); + this.closeInlineMenu({ tab: focusedFieldTab }, { forceCloseInlineMenu: true }); } /** @@ -217,6 +710,17 @@ class OverlayBackground implements OverlayBackgroundInterface { details: message.details, }; + if (pageDetails.frameId !== 0 && pageDetails.details.fields.length) { + this.buildSubFrameOffsets( + pageDetails.tab, + pageDetails.frameId, + pageDetails.details.url, + ).catch((error) => this.logService.error(error)); + BrowserApi.tabSendMessage(pageDetails.tab, { + command: "setupRebuildSubFrameOffsetsListeners", + }).catch((error) => this.logService.error(error)); + } + const pageDetailsMap = this.pageDetailsForTab[sender.tab.id]; if (!pageDetailsMap) { this.pageDetailsForTab[sender.tab.id] = new Map([[sender.frameId, pageDetails]]); @@ -227,22 +731,221 @@ class OverlayBackground implements OverlayBackgroundInterface { } /** - * Triggers autofill for the selected cipher in the overlay list. Also places - * the selected cipher at the top of the list of ciphers. + * Returns the frameId, called when calculating sub frame offsets within the tab. + * Is used to determine if we should reposition the inline menu when a resize event + * occurs within a frame. * - * @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID. - * @param sender - The sender of the port message + * @param sender - The sender of the message */ - private async fillSelectedOverlayListItem( - { overlayCipherId }: OverlayPortMessage, - { sender }: chrome.runtime.Port, + private getSenderFrameId(sender: chrome.runtime.MessageSender) { + return sender.frameId; + } + + /** + * Handles sub frame offset calculations for the given tab and frame id. + * Is used in setting the position of the inline menu list and button. + * + * @param message - The message received from the `updateSubFrameData` command + * @param sender - The sender of the message + */ + private updateSubFrameData( + message: OverlayBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, ) { - const pageDetails = this.pageDetailsForTab[sender.tab.id]; - if (!overlayCipherId || !pageDetails?.size) { + const subFrameOffsetsForTab = this.subFrameOffsetsForTab[sender.tab.id]; + if (subFrameOffsetsForTab) { + subFrameOffsetsForTab.set(message.subFrameData.frameId, message.subFrameData); + } + } + + /** + * Builds the offset data for a sub frame of a tab. The offset data is used + * to calculate the position of the inline menu list and button. + * + * @param tab - The tab that the sub frame is associated with + * @param frameId - The frame ID of the sub frame + * @param url - The URL of the sub frame + * @param forceRebuild - Identifies whether the sub frame offsets should be rebuilt + */ + private async buildSubFrameOffsets( + tab: chrome.tabs.Tab, + frameId: number, + url: string, + forceRebuild: boolean = false, + ) { + let subFrameDepth = 0; + const tabId = tab.id; + let subFrameOffsetsForTab = this.subFrameOffsetsForTab[tabId]; + if (!subFrameOffsetsForTab) { + this.subFrameOffsetsForTab[tabId] = new Map(); + subFrameOffsetsForTab = this.subFrameOffsetsForTab[tabId]; + } + + if (!forceRebuild && subFrameOffsetsForTab.get(frameId)) { return; } - const cipher = this.overlayLoginCiphers.get(overlayCipherId); + const subFrameData: SubFrameOffsetData = { url, top: 0, left: 0, parentFrameIds: [0] }; + let frameDetails = await BrowserApi.getFrameDetails({ tabId, frameId }); + + while (frameDetails && frameDetails.parentFrameId > -1) { + subFrameDepth++; + if (subFrameDepth >= MAX_SUB_FRAME_DEPTH) { + subFrameOffsetsForTab.set(frameId, null); + this.triggerDestroyInlineMenuListeners(tab, frameId); + return; + } + + const subFrameOffset: SubFrameOffsetData = await BrowserApi.tabSendMessage( + tab, + { + command: "getSubFrameOffsets", + subFrameUrl: frameDetails.url, + subFrameId: frameDetails.documentId, + }, + { frameId: frameDetails.parentFrameId }, + ); + + if (!subFrameOffset) { + subFrameOffsetsForTab.set(frameId, null); + BrowserApi.tabSendMessage( + tab, + { command: "getSubFrameOffsetsFromWindowMessage", subFrameId: frameId }, + { frameId }, + ).catch((error) => this.logService.error(error)); + return; + } + + subFrameData.top += subFrameOffset.top; + subFrameData.left += subFrameOffset.left; + if (!subFrameData.parentFrameIds.includes(frameDetails.parentFrameId)) { + subFrameData.parentFrameIds.push(frameDetails.parentFrameId); + } + + frameDetails = await BrowserApi.getFrameDetails({ + tabId, + frameId: frameDetails.parentFrameId, + }); + } + + subFrameOffsetsForTab.set(frameId, subFrameData); + } + + /** + * Triggers a removal and destruction of all + * + * @param tab - The tab that the sub frame is associated with + * @param frameId - The frame ID of the sub frame + */ + private triggerDestroyInlineMenuListeners(tab: chrome.tabs.Tab, frameId: number) { + this.logService.error( + "Excessive frame depth encountered, destroying inline menu on field within frame", + tab, + frameId, + ); + + BrowserApi.tabSendMessage( + tab, + { command: "destroyAutofillInlineMenuListeners" }, + { frameId }, + ).catch((error) => this.logService.error(error)); + } + + /** + * Rebuilds the sub frame offsets for the tab associated with the sender. + * + * @param sender - The sender of the message + */ + private async rebuildSubFrameOffsets(sender: chrome.runtime.MessageSender) { + this.cancelUpdateInlineMenuPositionSubject.next(); + this.clearDelayedInlineMenuClosure(); + + const subFrameOffsetsForTab = this.subFrameOffsetsForTab[sender.tab.id]; + if (subFrameOffsetsForTab) { + const tabFrameIds = Array.from(subFrameOffsetsForTab.keys()); + for (const frameId of tabFrameIds) { + await this.buildSubFrameOffsets(sender.tab, frameId, sender.url, true); + } + } + } + + /** + * Handles updating the inline menu's position after rebuilding the sub frames + * for the provided tab. Will skip repositioning the inline menu if the field + * is not currently focused, or if the focused field has a value. + * + * @param sender - The sender of the message + */ + private async updateInlineMenuPositionAfterRepositionEvent( + sender: chrome.runtime.MessageSender | void, + ) { + if (!sender || !this.isFieldCurrentlyFocused) { + return; + } + + if (!this.checkIsInlineMenuButtonVisible()) { + this.toggleInlineMenuHidden( + { isInlineMenuHidden: false, setTransparentInlineMenu: true }, + sender, + ).catch((error) => this.logService.error(error)); + } + + this.updateInlineMenuPosition({ overlayElement: AutofillOverlayElement.Button }, sender).catch( + (error) => this.logService.error(error), + ); + + const mostRecentlyFocusedFieldHasValue = await BrowserApi.tabSendMessage( + sender.tab, + { command: "checkMostRecentlyFocusedFieldHasValue" }, + { frameId: this.focusedFieldData?.frameId }, + ); + + if ((await this.getInlineMenuVisibility()) === AutofillOverlayVisibility.OnButtonClick) { + return; + } + + if ( + mostRecentlyFocusedFieldHasValue && + (this.checkIsInlineMenuCiphersPopulated(sender) || + (await this.getAuthStatus()) !== AuthenticationStatus.Unlocked) + ) { + return; + } + + this.updateInlineMenuPosition({ overlayElement: AutofillOverlayElement.List }, sender).catch( + (error) => this.logService.error(error), + ); + } + + /** + * Triggers autofill for the selected cipher in the inline menu list. Also places + * 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, usePasskey }: OverlayPortMessage, + { sender }: chrome.runtime.Port, + ) { + const pageDetails = this.pageDetailsForTab[sender.tab.id]; + if (!inlineMenuCipherId || !pageDetails?.size) { + return; + } + + const cipher = this.inlineMenuCiphers.get(inlineMenuCipherId); + + if (usePasskey && cipher.login?.hasFido2Credentials) { + await this.authenticatePasskeyCredential( + sender.tab.id, + cipher.login.fido2Credentials[0].credentialId, + ); + this.updateLastUsedInlineMenuCipher(inlineMenuCipherId, cipher); + this.closeInlineMenu(sender, { forceCloseInlineMenu: true }); + + return; + } if (await this.autofillService.isPasswordRepromptRequired(cipher, sender.tab)) { return; @@ -259,47 +962,149 @@ class OverlayBackground implements OverlayBackgroundInterface { this.platformUtilsService.copyToClipboard(totpCode); } - this.overlayLoginCiphers = new Map([[overlayCipherId, cipher], ...this.overlayLoginCiphers]); + this.updateLastUsedInlineMenuCipher(inlineMenuCipherId, cipher); } /** - * Checks if the overlay is focused. Will check the overlay list - * if it is open, otherwise it will check the overlay button. + * Triggers a FIDO2 authentication from the inline menu using the passed credential ID. + * + * @param tabId - The tab ID to trigger the authentication for + * @param credentialId - The credential ID to authenticate */ - private checkOverlayFocused() { - if (this.overlayListPort) { - this.checkOverlayListFocused(); + async authenticatePasskeyCredential(tabId: number, credentialId: string) { + const request = this.fido2ActiveRequestManager.getActiveRequest(tabId); + if (!request) { + this.logService.error( + "Could not complete passkey autofill due to missing active Fido2 request", + ); + return; + } + + request.subject.next({ type: Fido2ActiveRequestEvents.Continue, credentialId }); + } + + /** + * 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]); + } + + /** + * Checks if the inline menu is focused. Will check the inline menu list + * if it is open, otherwise it will check the inline menu button. + */ + private checkInlineMenuFocused(sender: chrome.runtime.MessageSender) { + if (!this.senderTabHasFocusedField(sender)) { + return; + } + + if (this.inlineMenuListPort) { + this.checkInlineMenuListFocused(); return; } - this.checkOverlayButtonFocused(); + this.checkInlineMenuButtonFocused(); } /** - * Posts a message to the overlay button iframe to check if it is focused. + * Posts a message to the inline menu button iframe to check if it is focused. */ - private checkOverlayButtonFocused() { - this.overlayButtonPort?.postMessage({ command: "checkAutofillOverlayButtonFocused" }); + private checkInlineMenuButtonFocused() { + this.inlineMenuButtonPort?.postMessage({ command: "checkAutofillInlineMenuButtonFocused" }); } /** - * Posts a message to the overlay list iframe to check if it is focused. + * Posts a message to the inline menu list iframe to check if it is focused. */ - private checkOverlayListFocused() { - this.overlayListPort?.postMessage({ command: "checkAutofillOverlayListFocused" }); + private checkInlineMenuListFocused() { + this.inlineMenuListPort?.postMessage({ command: "checkAutofillInlineMenuListFocused" }); } /** - * Sends a message to the sender tab to close the autofill overlay. + * Sends a message to the sender tab to close the autofill inline menu. * * @param sender - The sender of the port message - * @param forceCloseOverlay - Identifies whether the overlay should be force closed + * @param forceCloseInlineMenu - Identifies whether the inline menu should be forced closed + * @param overlayElement - The overlay element to close, either the list or button */ - private closeOverlay({ sender }: chrome.runtime.Port, forceCloseOverlay = 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 - BrowserApi.tabSendMessageData(sender.tab, "closeAutofillOverlay", { forceCloseOverlay }); + private closeInlineMenu( + sender: chrome.runtime.MessageSender, + { forceCloseInlineMenu, overlayElement }: CloseInlineMenuMessage = {}, + ) { + const command = "closeAutofillInlineMenu"; + const sendOptions = { frameId: 0 }; + if (forceCloseInlineMenu) { + BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions).catch( + (error) => this.logService.error(error), + ); + this.isInlineMenuButtonVisible = false; + this.isInlineMenuListVisible = false; + return; + } + + if (this.isFieldCurrentlyFocused) { + return; + } + + if (this.isFieldCurrentlyFilling) { + BrowserApi.tabSendMessage( + sender.tab, + { command, overlayElement: AutofillOverlayElement.List }, + sendOptions, + ).catch((error) => this.logService.error(error)); + this.isInlineMenuListVisible = false; + return; + } + + if (overlayElement === AutofillOverlayElement.Button) { + this.isInlineMenuButtonVisible = false; + } + + if (overlayElement === AutofillOverlayElement.List) { + this.isInlineMenuListVisible = false; + } + + if (!overlayElement) { + this.isInlineMenuButtonVisible = false; + this.isInlineMenuListVisible = false; + } + + BrowserApi.tabSendMessage(sender.tab, { command, overlayElement }, sendOptions).catch((error) => + this.logService.error(error), + ); + } + + /** + * Sends a message to the sender tab to trigger a delayed closure of the inline menu. + * This is used to ensure that we capture click events on the inline menu in the case + * that some on page programmatic method attempts to force focus redirection. + */ + private triggerDelayedInlineMenuClosure() { + if (this.isFieldCurrentlyFocused) { + return; + } + + this.clearDelayedInlineMenuClosure(); + this.delayedCloseTimeout = globalThis.setTimeout(() => { + const message = { command: "triggerDelayedAutofillInlineMenuClosure" }; + this.inlineMenuButtonPort?.postMessage(message); + this.inlineMenuListPort?.postMessage(message); + }, 100); + } + + /** + * Clears the delayed closure timeout for the inline menu, effectively + * cancelling the event from occurring. + */ + private clearDelayedInlineMenuClosure() { + if (this.delayedCloseTimeout) { + clearTimeout(this.delayedCloseTimeout); + } } /** @@ -313,61 +1118,148 @@ class OverlayBackground implements OverlayBackgroundInterface { { overlayElement }: OverlayBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ) { - if (sender.tab.id !== this.focusedFieldData?.tabId) { + if (!this.senderTabHasFocusedField(sender)) { this.expiredPorts.forEach((port) => port.disconnect()); this.expiredPorts = []; + return; } if (overlayElement === AutofillOverlayElement.Button) { - this.overlayButtonPort?.disconnect(); - this.overlayButtonPort = null; + this.inlineMenuButtonPort?.disconnect(); + this.inlineMenuButtonPort = null; + this.isInlineMenuButtonVisible = false; return; } - this.overlayListPort?.disconnect(); - this.overlayListPort = null; + this.inlineMenuListPort?.disconnect(); + this.inlineMenuListPort = null; + this.isInlineMenuListVisible = false; } /** - * Updates the position of either the overlay list or button. The position + * Updates the position of either the inline menu list or button. The position * is based on the focused field's position and dimensions. * * @param overlayElement - The overlay element to update, either the list or button * @param sender - The sender of the port message */ - private updateOverlayPosition( + private async updateInlineMenuPosition( { overlayElement }: { overlayElement?: string }, sender: chrome.runtime.MessageSender, ) { - if (!overlayElement || sender.tab.id !== this.focusedFieldData?.tabId) { + if (!overlayElement || !this.senderTabHasFocusedField(sender)) { return; } + this.cancelInlineMenuFadeInAndPositionUpdate(); + + await BrowserApi.tabSendMessage( + sender.tab, + { command: "appendAutofillInlineMenuToDom", overlayElement }, + { frameId: 0 }, + ); + + const subFrameOffsetsForTab = this.subFrameOffsetsForTab[this.focusedFieldData.tabId]; + let subFrameOffsets: SubFrameOffsetData; + if (subFrameOffsetsForTab) { + subFrameOffsets = subFrameOffsetsForTab.get(this.focusedFieldData.frameId); + if (subFrameOffsets === null) { + this.rebuildSubFrameOffsetsSubject.next(sender); + this.startUpdateInlineMenuPositionSubject.next(sender); + return; + } + } + if (overlayElement === AutofillOverlayElement.Button) { - this.overlayButtonPort?.postMessage({ - command: "updateIframePosition", - styles: this.getOverlayButtonPosition(), + this.inlineMenuButtonPort?.postMessage({ + command: "updateAutofillInlineMenuPosition", + styles: this.getInlineMenuButtonPosition(subFrameOffsets), }); + this.startInlineMenuFadeIn(); return; } - this.overlayListPort?.postMessage({ - command: "updateIframePosition", - styles: this.getOverlayListPosition(), + this.inlineMenuListPort?.postMessage({ + command: "updateAutofillInlineMenuPosition", + styles: this.getInlineMenuListPosition(subFrameOffsets), }); + this.startInlineMenuFadeIn(); + } + + /** + * Triggers an update of the inline menu's visibility after the top level frame + * appends the element to the DOM. + * + * @param message - The message received from the content script + * @param sender - The sender of the port message + */ + private updateInlineMenuElementIsVisibleStatus( + message: OverlayBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + if (!this.senderTabHasFocusedField(sender)) { + return; + } + + const { overlayElement, isVisible } = message; + if (overlayElement === AutofillOverlayElement.Button) { + this.isInlineMenuButtonVisible = isVisible; + return; + } + + if (overlayElement === AutofillOverlayElement.List) { + this.isInlineMenuListVisible = isVisible; + } + } + + /** + * Returns the position of the currently open inline menu. + */ + private getInlineMenuPosition(): InlineMenuPosition { + return this.inlineMenuPosition; + } + + /** + * Handles updating the opacity of both the inline menu button and list. + * This is used to simultaneously fade in the inline menu elements. + */ + private startInlineMenuFadeIn() { + this.cancelInlineMenuFadeIn(); + this.startInlineMenuFadeInSubject.next(); + } + + /** + * Clears the timeout used to fade in the inline menu elements. + */ + private cancelInlineMenuFadeIn() { + this.cancelInlineMenuFadeInSubject.next(true); + } + + /** + * Posts a message to the inline menu elements to trigger a fade in of the inline menu. + * + * @param cancelFadeIn - Signal passed to debounced observable to cancel the fade in + */ + private async triggerInlineMenuFadeIn(cancelFadeIn: boolean = false) { + if (cancelFadeIn) { + return; + } + + const message = { command: "fadeInAutofillInlineMenuIframe" }; + this.inlineMenuButtonPort?.postMessage(message); + this.inlineMenuListPort?.postMessage(message); } /** * Gets the position of the focused field and calculates the position - * of the overlay button based on the focused field's position and dimensions. + * of the inline menu button based on the focused field's position and dimensions. */ - private getOverlayButtonPosition() { - if (!this.focusedFieldData) { - return; - } + private getInlineMenuButtonPosition(subFrameOffsets: SubFrameOffsetData) { + const subFrameTopOffset = subFrameOffsets?.top || 0; + const subFrameLeftOffset = subFrameOffsets?.left || 0; const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; const { paddingRight, paddingLeft } = this.focusedFieldData.focusedFieldStyles; @@ -376,38 +1268,52 @@ class OverlayBackground implements OverlayBackgroundInterface { elementOffset = height >= 50 ? height * 0.47 : height * 0.42; } - const elementHeight = height - elementOffset; - const elementTopPosition = top + elementOffset / 2; - let elementLeftPosition = left + width - height + elementOffset / 2; - const fieldPaddingRight = parseInt(paddingRight, 10); const fieldPaddingLeft = parseInt(paddingLeft, 10); - if (fieldPaddingRight > fieldPaddingLeft) { - elementLeftPosition = left + width - height - (fieldPaddingRight - elementOffset + 2); - } + const elementHeight = height - elementOffset; + + const elementTopPosition = subFrameTopOffset + top + elementOffset / 2; + const elementLeftPosition = + fieldPaddingRight > fieldPaddingLeft + ? subFrameLeftOffset + left + width - height - (fieldPaddingRight - elementOffset + 2) + : subFrameLeftOffset + left + width - height + elementOffset / 2; + + this.inlineMenuPosition.button = { + top: Math.round(elementTopPosition), + left: Math.round(elementLeftPosition), + height: Math.round(elementHeight), + width: Math.round(elementHeight), + }; return { - top: `${Math.round(elementTopPosition)}px`, - left: `${Math.round(elementLeftPosition)}px`, - height: `${Math.round(elementHeight)}px`, - width: `${Math.round(elementHeight)}px`, + top: `${this.inlineMenuPosition.button.top}px`, + left: `${this.inlineMenuPosition.button.left}px`, + height: `${this.inlineMenuPosition.button.height}px`, + width: `${this.inlineMenuPosition.button.width}px`, }; } /** * Gets the position of the focused field and calculates the position - * of the overlay list based on the focused field's position and dimensions. + * of the inline menu list based on the focused field's position and dimensions. */ - private getOverlayListPosition() { - if (!this.focusedFieldData) { - return; - } + private getInlineMenuListPosition(subFrameOffsets: SubFrameOffsetData) { + const subFrameTopOffset = subFrameOffsets?.top || 0; + const subFrameLeftOffset = subFrameOffsets?.left || 0; const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; + + this.inlineMenuPosition.list = { + top: Math.round(top + height + subFrameTopOffset), + left: Math.round(left + subFrameLeftOffset), + height: 0, + width: Math.round(width), + }; + return { - width: `${Math.round(width)}px`, - top: `${Math.round(top + height)}px`, - left: `${Math.round(left)}px`, + width: `${this.inlineMenuPosition.list.width}px`, + top: `${this.inlineMenuPosition.list.top}px`, + left: `${this.inlineMenuPosition.list.left}px`, }; } @@ -421,109 +1327,181 @@ class OverlayBackground implements OverlayBackgroundInterface { { focusedFieldData }: OverlayBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ) { - this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id }; - } + if (this.focusedFieldData && !this.senderFrameHasFocusedField(sender)) { + BrowserApi.tabSendMessage( + sender.tab, + { command: "unsetMostRecentlyFocusedField" }, + { frameId: this.focusedFieldData.frameId }, + ).catch((error) => this.logService.error(error)); + } - /** - * Updates the overlay's visibility based on the display property passed in the extension message. - * - * @param display - The display property of the overlay, either "block" or "none" - */ - private updateOverlayHidden({ display }: OverlayBackgroundExtensionMessage) { - if (!display) { + const previousFocusedFieldData = this.focusedFieldData; + this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id, frameId: sender.frameId }; + this.isFieldCurrentlyFocused = true; + + const accountCreationFieldBlurred = + previousFocusedFieldData?.showInlineMenuAccountCreation && + !this.focusedFieldData.showInlineMenuAccountCreation; + + if (accountCreationFieldBlurred || this.showInlineMenuAccountCreation()) { + this.updateIdentityCiphersOnLoginField(previousFocusedFieldData).catch((error) => + this.logService.error(error), + ); return; } - const portMessage = { command: "updateOverlayHidden", styles: { display } }; - - this.overlayButtonPort?.postMessage(portMessage); - this.overlayListPort?.postMessage(portMessage); + if (previousFocusedFieldData?.filledByCipherType !== focusedFieldData?.filledByCipherType) { + const updateAllCipherTypes = focusedFieldData.filledByCipherType !== CipherType.Login; + this.updateOverlayCiphers(updateAllCipherTypes).catch((error) => + this.logService.error(error), + ); + } } /** - * Sends a message to the currently active tab to open the autofill overlay. + * Triggers an update of populated identity ciphers when a login field is focused. * - * @param isFocusingFieldElement - Identifies whether the field element should be focused when the overlay is opened - * @param isOpeningFullOverlay - Identifies whether the full overlay should be forced open regardless of other states + * @param previousFocusedFieldData - The data set of the previously focused field */ - private async openOverlay(isFocusingFieldElement = false, isOpeningFullOverlay = false) { - const currentTab = await BrowserApi.getTabFromCurrentWindowId(); + private async updateIdentityCiphersOnLoginField(previousFocusedFieldData: FocusedFieldData) { + if ( + !previousFocusedFieldData || + !this.isInlineMenuButtonVisible || + (await this.getAuthStatus()) !== AuthenticationStatus.Unlocked + ) { + return; + } - await BrowserApi.tabSendMessageData(currentTab, "openAutofillOverlay", { - isFocusingFieldElement, - isOpeningFullOverlay, + this.inlineMenuListPort?.postMessage({ + command: "updateAutofillInlineMenuListCiphers", + ciphers: await this.getInlineMenuCipherData(), + showInlineMenuAccountCreation: this.showInlineMenuAccountCreation(), + showPasskeysLabels: this.showPasskeysLabelsWithinInlineMenu, + }); + } + + /** + * Updates the inline menu's visibility based on the display property passed in the extension message. + * + * @param display - The display property of the inline menu, either "block" or "none" + * @param sender - The sender of the extension message + */ + private async toggleInlineMenuHidden( + { isInlineMenuHidden, setTransparentInlineMenu }: ToggleInlineMenuHiddenMessage, + sender: chrome.runtime.MessageSender, + ) { + if (!this.senderTabHasFocusedField(sender)) { + return; + } + + this.cancelInlineMenuFadeIn(); + const display = isInlineMenuHidden ? "none" : "block"; + let styles: { display: string; opacity?: string } = { display }; + + if (typeof setTransparentInlineMenu !== "undefined") { + const opacity = setTransparentInlineMenu ? "0" : "1"; + styles = { ...styles, opacity }; + } + + const portMessage = { command: "toggleAutofillInlineMenuHidden", styles }; + if (this.inlineMenuButtonPort) { + this.isInlineMenuButtonVisible = !isInlineMenuHidden; + this.inlineMenuButtonPort.postMessage(portMessage); + } + + if (this.inlineMenuListPort) { + this.isInlineMenuListVisible = !isInlineMenuHidden; + this.inlineMenuListPort.postMessage(portMessage); + } + + if (setTransparentInlineMenu) { + this.startInlineMenuFadeIn(); + } + } + + /** + * Sends a message to the currently active tab to open the autofill inline menu. + * + * @param isFocusingFieldElement - Identifies whether the field element should be focused when the inline menu is opened + * @param isOpeningFullInlineMenu - Identifies whether the full inline menu should be forced open regardless of other states + */ + private async openInlineMenu(isFocusingFieldElement = false, isOpeningFullInlineMenu = false) { + this.clearDelayedInlineMenuClosure(); + const currentTab = await BrowserApi.getTabFromCurrentWindowId(); + if (!currentTab) { + return; + } + + await BrowserApi.tabSendMessage( + currentTab, + { + command: "openAutofillInlineMenu", + isFocusingFieldElement, + isOpeningFullInlineMenu, + authStatus: await this.getAuthStatus(), + }, + { + frameId: this.focusedFieldData?.tabId === currentTab.id ? this.focusedFieldData.frameId : 0, + }, + ); + } + + /** + * Gets the inline menu's visibility setting from the settings service. + */ + private async getInlineMenuVisibility(): Promise { + return await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$); + } + + /** + * Gets the user's authentication status from the auth service. If the user's authentication + * status has changed, the inline menu button's authentication status will be updated + * and the inline menu list's ciphers will be updated. + */ + private async getAuthStatus() { + return await firstValueFrom(this.authService.activeAccountStatus$); + } + + /** + * Sends a message to the inline menu button to update its authentication status. + */ + private async updateInlineMenuButtonAuthStatus() { + this.inlineMenuButtonPort?.postMessage({ + command: "updateInlineMenuButtonAuthStatus", authStatus: await this.getAuthStatus(), }); } /** - * Gets the overlay's visibility setting from the settings service. - */ - private async getOverlayVisibility(): Promise { - return await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$); - } - - /** - * Gets the user's authentication status from the auth service. If the user's - * authentication status has changed, the overlay button's authentication status - * will be updated and the overlay list's ciphers will be updated. - */ - private async getAuthStatus() { - const formerAuthStatus = this.userAuthStatus; - this.userAuthStatus = await this.authService.getAuthStatus(); - - if ( - this.userAuthStatus !== formerAuthStatus && - this.userAuthStatus === AuthenticationStatus.Unlocked - ) { - this.updateOverlayButtonAuthStatus(); - await this.updateOverlayCiphers(); - } - - return this.userAuthStatus; - } - - /** - * Sends a message to the overlay button to update its authentication status. - */ - private updateOverlayButtonAuthStatus() { - this.overlayButtonPort?.postMessage({ - command: "updateOverlayButtonAuthStatus", - authStatus: this.userAuthStatus, - }); - } - - /** - * Handles the overlay button being clicked. If the user is not authenticated, - * the vault will be unlocked. If the user is authenticated, the overlay will + * Handles the inline menu button being clicked. If the user is not authenticated, + * the vault will be unlocked. If the user is authenticated, the inline menu will * be opened. * - * @param port - The port of the overlay button + * @param port - The port of the inline menu button */ - private handleOverlayButtonClicked(port: chrome.runtime.Port) { - if (this.userAuthStatus !== AuthenticationStatus.Unlocked) { - // 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.unlockVault(port); + private async handleInlineMenuButtonClicked(port: chrome.runtime.Port) { + this.clearDelayedInlineMenuClosure(); + this.cancelInlineMenuFadeInAndPositionUpdate(); + + if ((await this.getAuthStatus()) !== AuthenticationStatus.Unlocked) { + await this.unlockVault(port); return; } - // 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.openOverlay(false, true); + await this.openInlineMenu(false, true); } /** * Facilitates opening the unlock popout window. * - * @param port - The port of the overlay list + * @param port - The port of the inline menu list */ private async unlockVault(port: chrome.runtime.Port) { const { sender } = port; - this.closeOverlay(port); + this.closeInlineMenu(port.sender); const retryMessage: LockedVaultPendingNotificationsData = { - commandToRetry: { message: { command: "openAutofillOverlay" }, sender }, + commandToRetry: { message: { command: "openAutofillInlineMenu" }, sender }, target: "overlay.background", }; await BrowserApi.tabSendMessageData( @@ -537,18 +1515,19 @@ class OverlayBackground implements OverlayBackgroundInterface { /** * Triggers the opening of a vault item popout window associated * with the passed cipher ID. - * @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID. + * @param inlineMenuCipherId - Cipher ID corresponding to the inlineMenuCiphers map. Does not correspond to the actual cipher's ID. * @param sender - The sender of the port message */ private async viewSelectedCipher( - { overlayCipherId }: OverlayPortMessage, + { inlineMenuCipherId }: OverlayPortMessage, { sender }: chrome.runtime.Port, ) { - const cipher = this.overlayLoginCiphers.get(overlayCipherId); + const cipher = this.inlineMenuCiphers.get(inlineMenuCipherId); if (!cipher) { return; } + this.closeInlineMenu(sender); await this.openViewVaultItemPopout(sender.tab, { cipherId: cipher.id, action: SHOW_AUTOFILL_BUTTON, @@ -556,59 +1535,71 @@ class OverlayBackground implements OverlayBackgroundInterface { } /** - * Facilitates redirecting focus to the overlay list. + * Facilitates redirecting focus to the inline menu list. */ - private focusOverlayList() { - this.overlayListPort?.postMessage({ command: "focusOverlayList" }); + private focusInlineMenuList() { + this.inlineMenuListPort?.postMessage({ command: "focusAutofillInlineMenuList" }); } /** - * Updates the authentication status for the user and opens the overlay if + * Updates the authentication status for the user and opens the inline menu if * a followup command is present in the message. * * @param message - Extension message received from the `unlockCompleted` command */ private async unlockCompleted(message: OverlayBackgroundExtensionMessage) { - await this.getAuthStatus(); + await this.updateInlineMenuButtonAuthStatus(); + await this.updateOverlayCiphers(); - if (message.data?.commandToRetry?.message?.command === "openAutofillOverlay") { - await this.openOverlay(true); + if (message.data?.commandToRetry?.message?.command === "openAutofillInlineMenu") { + await this.openInlineMenu(true); } } /** - * Gets the translations for the overlay page. + * Gets the translations for the inline menu page. */ - private getTranslations() { - if (!this.overlayPageTranslations) { - this.overlayPageTranslations = { + private getInlineMenuTranslations() { + if (!this.inlineMenuPageTranslations) { + this.inlineMenuPageTranslations = { locale: BrowserApi.getUILanguage(), opensInANewWindow: this.i18nService.translate("opensInANewWindow"), buttonPageTitle: this.i18nService.translate("bitwardenOverlayButton"), toggleBitwardenVaultOverlay: this.i18nService.translate("toggleBitwardenVaultOverlay"), listPageTitle: this.i18nService.translate("bitwardenVault"), - unlockYourAccount: this.i18nService.translate("unlockYourAccountToViewMatchingLogins"), + unlockYourAccount: this.i18nService.translate("unlockYourAccountToViewAutofillSuggestions"), unlockAccount: this.i18nService.translate("unlockAccount"), + unlockAccountAria: this.i18nService.translate("unlockAccountAria"), fillCredentialsFor: this.i18nService.translate("fillCredentialsFor"), - partialUsername: this.i18nService.translate("partialUsername"), + username: this.i18nService.translate("username")?.toLowerCase(), view: this.i18nService.translate("view"), noItemsToShow: this.i18nService.translate("noItemsToShow"), newItem: this.i18nService.translate("newItem"), addNewVaultItem: this.i18nService.translate("addNewVaultItem"), + newLogin: this.i18nService.translate("newLogin"), + addNewLoginItem: this.i18nService.translate("addNewLoginItemAria"), + newCard: this.i18nService.translate("newCard"), + addNewCardItem: this.i18nService.translate("addNewCardItemAria"), + 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"), }; } - return this.overlayPageTranslations; + return this.inlineMenuPageTranslations; } /** * Facilitates redirecting focus out of one of the - * overlay elements to elements on the page. + * inline menu elements to elements on the page. * * @param direction - The direction to redirect focus to (either "next", "previous" or "current) * @param sender - The sender of the port message */ - private redirectOverlayFocusOut( + private redirectInlineMenuFocusOut( { direction }: OverlayPortMessage, { sender }: chrome.runtime.Port, ) { @@ -616,36 +1607,271 @@ class OverlayBackground implements OverlayBackgroundInterface { return; } - // 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 - BrowserApi.tabSendMessageData(sender.tab, "redirectOverlayFocusOut", { direction }); + BrowserApi.tabSendMessageData(sender.tab, "redirectAutofillInlineMenuFocusOut", { + direction, + }).catch((error) => this.logService.error(error)); } /** * Triggers adding a new vault item from the overlay. Gathers data * input by the user before calling to open the add/edit window. * + * @param addNewCipherType - The type of cipher to add * @param sender - The sender of the port message */ - private getNewVaultItemDetails({ sender }: chrome.runtime.Port) { - void BrowserApi.tabSendMessage(sender.tab, { command: "addNewVaultItemFromOverlay" }); + private getNewVaultItemDetails( + { addNewCipherType }: OverlayPortMessage, + { sender }: chrome.runtime.Port, + ) { + if (!addNewCipherType || !this.senderTabHasFocusedField(sender)) { + return; + } + + this.currentAddNewItemData = { addNewCipherType, sender }; + BrowserApi.tabSendMessage(sender.tab, { + command: "addNewVaultItemFromOverlay", + addNewCipherType, + }).catch((error) => this.logService.error(error)); } /** * Handles adding a new vault item from the overlay. Gathers data login * data captured in the extension message. * + * @param addNewCipherType - The type of cipher to add * @param login - The login data captured from the extension message + * @param card - The card data captured from the extension message + * @param identity - The identity data captured from the extension message * @param sender - The sender of the extension message */ private async addNewVaultItem( - { login }: OverlayAddNewItemMessage, + { addNewCipherType, login, card, identity }: OverlayAddNewItemMessage, sender: chrome.runtime.MessageSender, ) { - if (!login) { + if ( + !this.currentAddNewItemData || + sender.tab.id !== this.currentAddNewItemData.sender.tab.id || + !addNewCipherType || + this.currentAddNewItemData.addNewCipherType !== addNewCipherType + ) { return; } + if (login && this.isAddingNewLogin()) { + this.updateCurrentAddNewItemLogin(login, sender); + } + + if (card && this.isAddingNewCard()) { + this.updateCurrentAddNewItemCard(card); + } + + if (identity && this.isAddingNewIdentity()) { + this.updateCurrentAddNewItemIdentity(identity); + } + + this.addNewVaultItemSubject.next(this.currentAddNewItemData); + } + + /** + * Identifies if the current add new item data is for adding a new login. + */ + private isAddingNewLogin() { + return this.currentAddNewItemData.addNewCipherType === CipherType.Login; + } + + /** + * Identifies if the current add new item data is for adding a new card. + */ + private isAddingNewCard() { + return this.currentAddNewItemData.addNewCipherType === CipherType.Card; + } + + /** + * Identifies if the current add new item data is for adding a new identity. + */ + private isAddingNewIdentity() { + return this.currentAddNewItemData.addNewCipherType === CipherType.Identity; + } + + /** + * Updates the current add new item data with the provided login data. If the + * login data is already present, the data will be merged with the existing data. + * + * @param login - The login data captured from the extension message + * @param sender - The sender of the extension message + */ + private updateCurrentAddNewItemLogin( + login: NewLoginCipherData, + sender: chrome.runtime.MessageSender, + ) { + const { username, password } = login; + + if (this.partialLoginDataFoundInSubFrame(sender, login)) { + login.uri = ""; + login.hostname = ""; + } + + if (!this.currentAddNewItemData.login) { + this.currentAddNewItemData.login = login; + return; + } + + const currentLoginData = this.currentAddNewItemData.login; + if (sender.frameId === 0 && currentLoginData.hostname && !username && !password) { + login.uri = ""; + login.hostname = ""; + } + + this.currentAddNewItemData.login = { + uri: login.uri || currentLoginData.uri, + hostname: login.hostname || currentLoginData.hostname, + username: username || currentLoginData.username, + password: password || currentLoginData.password, + }; + } + + /** + * Handles verifying if the login data for a tab is separated between various + * iframe elements. If that is the case, we want to ignore the login uri and + * domain to ensure the top frame is treated as the primary source of login data. + * + * @param sender - The sender of the extension message + * @param login - The login data captured from the extension message + */ + private partialLoginDataFoundInSubFrame( + sender: chrome.runtime.MessageSender, + login: NewLoginCipherData, + ) { + const { frameId } = sender; + const { username, password } = login; + + return frameId !== 0 && (!username || !password); + } + + /** + * Updates the current add new item data with the provided card data. If the + * card data is already present, the data will be merged with the existing data. + * + * @param card - The card data captured from the extension message + */ + private updateCurrentAddNewItemCard(card: NewCardCipherData) { + if (!this.currentAddNewItemData.card) { + this.currentAddNewItemData.card = card; + return; + } + + const currentCardData = this.currentAddNewItemData.card; + this.currentAddNewItemData.card = { + cardholderName: card.cardholderName || currentCardData.cardholderName, + number: card.number || currentCardData.number, + expirationMonth: card.expirationMonth || currentCardData.expirationMonth, + expirationYear: card.expirationYear || currentCardData.expirationYear, + expirationDate: card.expirationDate || currentCardData.expirationDate, + cvv: card.cvv || currentCardData.cvv, + }; + } + + /** + * Updates the current add new item data with the provided identity data. If the + * identity data is already present, the data will be merged with the existing data. + * + * @param identity - The identity data captured from the extension message + */ + private updateCurrentAddNewItemIdentity(identity: NewIdentityCipherData) { + if (!this.currentAddNewItemData.identity) { + this.currentAddNewItemData.identity = identity; + return; + } + + const currentIdentityData = this.currentAddNewItemData.identity; + this.currentAddNewItemData.identity = { + title: identity.title || currentIdentityData.title, + firstName: identity.firstName || currentIdentityData.firstName, + middleName: identity.middleName || currentIdentityData.middleName, + lastName: identity.lastName || currentIdentityData.lastName, + fullName: identity.fullName || currentIdentityData.fullName, + address1: identity.address1 || currentIdentityData.address1, + address2: identity.address2 || currentIdentityData.address2, + address3: identity.address3 || currentIdentityData.address3, + city: identity.city || currentIdentityData.city, + state: identity.state || currentIdentityData.state, + postalCode: identity.postalCode || currentIdentityData.postalCode, + country: identity.country || currentIdentityData.country, + company: identity.company || currentIdentityData.company, + phone: identity.phone || currentIdentityData.phone, + email: identity.email || currentIdentityData.email, + username: identity.username || currentIdentityData.username, + }; + } + + /** + * Handles building a new cipher and opening the add/edit vault item popout. + * + * @param login - The login data captured from the extension message + * @param card - The card data captured from the extension message + * @param identity - The identity data captured from the extension message + * @param sender - The sender of the extension message + */ + private async buildCipherAndOpenAddEditVaultItemPopout({ + login, + card, + identity, + sender, + }: CurrentAddNewItemData) { + const cipherView: CipherView = this.buildNewVaultItemCipherView({ + login, + card, + identity, + }); + + if (!cipherView) { + this.currentAddNewItemData = null; + return; + } + + try { + this.closeInlineMenu(sender); + await this.cipherService.setAddEditCipherInfo({ + cipher: cipherView, + collectionIds: cipherView.collectionIds, + }); + + await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id }); + await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher"); + } catch (error) { + this.logService.error("Error building cipher and opening add/edit vault item popout", error); + } + + this.currentAddNewItemData = null; + } + + /** + * Builds and returns a new cipher view with the provided vault item data. + * + * @param login - The login data captured from the extension message + * @param card - The card data captured from the extension message + * @param identity - The identity data captured from the extension message + */ + private buildNewVaultItemCipherView({ login, card, identity }: OverlayAddNewItemMessage) { + if (login && this.isAddingNewLogin()) { + return this.buildLoginCipherView(login); + } + + if (card && this.isAddingNewCard()) { + return this.buildCardCipherView(card); + } + + if (identity && this.isAddingNewIdentity()) { + return this.buildIdentityCipherView(identity); + } + } + + /** + * Builds a new login cipher view with the provided login data. + * + * @param login - The login data captured from the extension message + */ + private buildLoginCipherView(login: NewLoginCipherData) { const uriView = new LoginUriView(); uriView.uri = login.uri; @@ -660,20 +1886,334 @@ class OverlayBackground implements OverlayBackgroundInterface { cipherView.type = CipherType.Login; cipherView.login = loginView; - await this.cipherService.setAddEditCipherInfo({ - cipher: cipherView, - collectionIds: cipherView.collectionIds, - }); + return cipherView; + } - await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id }); - await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher"); + /** + * Builds a new card cipher view with the provided card data. + * + * @param card - The card data captured from the extension message + */ + private buildCardCipherView(card: NewCardCipherData) { + const cardView = new CardView(); + cardView.cardholderName = card.cardholderName || ""; + cardView.number = card.number || ""; + cardView.expMonth = card.expirationMonth || ""; + cardView.expYear = card.expirationYear || ""; + cardView.code = card.cvv || ""; + cardView.brand = card.number ? CardView.getCardBrandByPatterns(card.number) : ""; + + const cipherView = new CipherView(); + cipherView.name = ""; + cipherView.folderId = null; + cipherView.type = CipherType.Card; + cipherView.card = cardView; + + return cipherView; + } + + /** + * Builds a new identity cipher view with the provided identity data. + * + * @param identity - The identity data captured from the extension message + */ + private buildIdentityCipherView(identity: NewIdentityCipherData) { + const identityView = new IdentityView(); + identityView.title = identity.title || ""; + identityView.firstName = identity.firstName || ""; + identityView.middleName = identity.middleName || ""; + identityView.lastName = identity.lastName || ""; + identityView.address1 = identity.address1 || ""; + identityView.address2 = identity.address2 || ""; + identityView.address3 = identity.address3 || ""; + identityView.city = identity.city || ""; + identityView.state = identity.state || ""; + identityView.postalCode = identity.postalCode || ""; + identityView.country = identity.country || ""; + identityView.company = identity.company || ""; + identityView.phone = identity.phone || ""; + identityView.email = identity.email || ""; + identityView.username = identity.username || ""; + + if (identity.fullName && !identityView.firstName && !identityView.lastName) { + this.buildIdentityNameParts(identity, identityView); + } + + const cipherView = new CipherView(); + cipherView.name = ""; + cipherView.folderId = null; + cipherView.type = CipherType.Identity; + cipherView.identity = identityView; + + return cipherView; + } + + /** + * Splits the identity full name into first, middle, and last name parts. + * + * @param identity - The identity data captured from the extension message + * @param identityView - The identity view to update + */ + private buildIdentityNameParts(identity: NewIdentityCipherData, identityView: IdentityView) { + const fullNameParts = identity.fullName.split(" "); + if (fullNameParts.length === 1) { + identityView.firstName = fullNameParts[0] || ""; + + return; + } + + if (fullNameParts.length === 2) { + identityView.firstName = fullNameParts[0] || ""; + identityView.lastName = fullNameParts[1] || ""; + + return; + } + + identityView.firstName = fullNameParts[0] || ""; + identityView.middleName = fullNameParts[1] || ""; + identityView.lastName = fullNameParts[2] || ""; + } + + /** + * Updates the property that identifies if a form field set up for the inline menu is currently focused. + * + * @param message - The message received from the web page + * @param sender - The sender of the port message + */ + private updateIsFieldCurrentlyFocused( + message: OverlayBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + if (this.focusedFieldData && !this.senderFrameHasFocusedField(sender)) { + return; + } + + this.isFieldCurrentlyFocused = message.isFieldCurrentlyFocused; + } + + /** + * Allows a content script to check if a form field setup for the inline menu is currently focused. + */ + private checkIsFieldCurrentlyFocused() { + return this.isFieldCurrentlyFocused; + } + + /** + * Updates the property that identifies if a form field is currently being autofilled. + * + * @param message - The message received from the web page + */ + private updateIsFieldCurrentlyFilling(message: OverlayBackgroundExtensionMessage) { + this.isFieldCurrentlyFilling = message.isFieldCurrentlyFilling; + } + + /** + * Allows a content script to check if a form field is currently being autofilled. + */ + private checkIsFieldCurrentlyFilling() { + return this.isFieldCurrentlyFilling; + } + + /** + * Returns the visibility status of the inline menu button. + */ + private checkIsInlineMenuButtonVisible(): boolean { + return this.isInlineMenuButtonVisible; + } + + /** + * Returns the visibility status of the inline menu list. + */ + private checkIsInlineMenuListVisible(): boolean { + return this.isInlineMenuListVisible; + } + + /** + * Responds to the content script's request to check if the inline menu ciphers are populated. + * This will return true only if the sender is the focused field's tab and the inline menu + * ciphers are populated. + * + * @param sender - The sender of the message + */ + private checkIsInlineMenuCiphersPopulated(sender: chrome.runtime.MessageSender) { + return this.senderTabHasFocusedField(sender) && this.currentInlineMenuCiphersCount > 0; + } + + /** + * Triggers an update in the meta "color-scheme" value within the inline menu button. + * This is done to ensure that the button element has a transparent background, which + * is accomplished by setting the "color-scheme" meta value of the button iframe to + * the same value as the page's meta "color-scheme" value. + */ + private updateInlineMenuButtonColorScheme() { + this.inlineMenuButtonPort?.postMessage({ + command: "updateAutofillInlineMenuColorScheme", + }); + } + + /** + * Triggers an update in the inline menu list's height. + * + * @param message - Contains the dimensions of the inline menu list + */ + private updateInlineMenuListHeight(message: OverlayBackgroundExtensionMessage) { + const parsedHeight = parseInt(message.styles?.height); + if (this.inlineMenuPosition.list && parsedHeight > 0) { + this.inlineMenuPosition.list.height = parsedHeight; + } + + this.inlineMenuListPort?.postMessage({ + command: "updateAutofillInlineMenuPosition", + styles: message.styles, + }); + } + + /** + * Handles verifying whether the inline menu should be repositioned. This is used to + * guard against removing the inline menu when other frames trigger a resize event. + * + * @param sender - The sender of the message + */ + private checkShouldRepositionInlineMenu(sender: chrome.runtime.MessageSender): boolean { + if (!this.focusedFieldData || !this.senderTabHasFocusedField(sender)) { + return false; + } + + if (this.senderFrameHasFocusedField(sender)) { + return true; + } + + const subFrameOffsetsForTab = this.subFrameOffsetsForTab[sender.tab.id]; + if (subFrameOffsetsForTab) { + for (const value of subFrameOffsetsForTab.values()) { + if (value?.parentFrameIds.includes(sender.frameId)) { + return true; + } + } + } + + return false; + } + + /** + * Identifies if the sender tab is the same as the focused field's tab. + * + * @param sender - The sender of the message + */ + private senderTabHasFocusedField(sender: chrome.runtime.MessageSender) { + return sender.tab.id === this.focusedFieldData?.tabId; + } + + /** + * Identifies if the sender frame is the same as the focused field's frame. + * + * @param sender - The sender of the message + */ + private senderFrameHasFocusedField(sender: chrome.runtime.MessageSender) { + return sender.frameId === this.focusedFieldData?.frameId; + } + + /** + * Triggers when a scroll or resize event occurs within a tab. Will reposition the inline menu + * if the focused field is within the viewport. + * + * @param sender - The sender of the message + */ + private async triggerOverlayReposition(sender: chrome.runtime.MessageSender) { + if (!this.checkShouldRepositionInlineMenu(sender)) { + return; + } + + this.resetFocusedFieldSubFrameOffsets(sender); + this.cancelInlineMenuFadeInAndPositionUpdate(); + this.toggleInlineMenuHidden({ isInlineMenuHidden: true }, sender).catch((error) => + this.logService.error(error), + ); + this.repositionInlineMenuSubject.next(sender); + } + + /** + * Sets the sub frame offsets for the currently focused field's frame to a null value . + * This ensures that we can delay presentation of the inline menu after a reposition + * event if the user clicks on a field before the sub frames can be rebuilt. + * + * @param sender + */ + private resetFocusedFieldSubFrameOffsets(sender: chrome.runtime.MessageSender) { + if (this.focusedFieldData.frameId > 0 && this.subFrameOffsetsForTab[sender.tab.id]) { + this.subFrameOffsetsForTab[sender.tab.id].set(this.focusedFieldData.frameId, null); + } + } + + /** + * Triggers when a focus event occurs within a tab. Will reposition the inline menu + * if the focused field is within the viewport. + * + * @param sender - The sender of the message + */ + private async triggerSubFrameFocusInRebuild(sender: chrome.runtime.MessageSender) { + this.cancelInlineMenuFadeInAndPositionUpdate(); + this.rebuildSubFrameOffsetsSubject.next(sender); + this.repositionInlineMenuSubject.next(sender); + } + + /** + * Handles determining if the inline menu should be repositioned or closed, and initiates + * the process of calculating the new position of the inline menu. + * + * @param sender - The sender of the message + */ + private repositionInlineMenu = async (sender: chrome.runtime.MessageSender) => { + this.cancelInlineMenuFadeInAndPositionUpdate(); + if (!this.isFieldCurrentlyFocused && !this.isInlineMenuButtonVisible) { + await this.closeInlineMenuAfterReposition(sender); + return; + } + + const isFieldWithinViewport = await BrowserApi.tabSendMessage( + sender.tab, + { command: "checkIsMostRecentlyFocusedFieldWithinViewport" }, + { frameId: this.focusedFieldData.frameId }, + ); + if (!isFieldWithinViewport) { + await this.closeInlineMenuAfterReposition(sender); + return; + } + + if (this.focusedFieldData.frameId > 0) { + this.rebuildSubFrameOffsetsSubject.next(sender); + } + + this.startUpdateInlineMenuPositionSubject.next(sender); + }; + + /** + * Triggers a closure of the inline menu during a reposition event. + * + * @param sender - The sender of the message +| */ + private async closeInlineMenuAfterReposition(sender: chrome.runtime.MessageSender) { + await this.toggleInlineMenuHidden( + { isInlineMenuHidden: false, setTransparentInlineMenu: true }, + sender, + ); + this.closeInlineMenu(sender, { forceCloseInlineMenu: true }); + } + + /** + * Cancels the observables that update the position and fade in of the inline menu. + */ + private cancelInlineMenuFadeInAndPositionUpdate() { + this.cancelInlineMenuFadeIn(); + this.cancelUpdateInlineMenuPositionSubject.next(); } /** * Sets up the extension message listeners for the overlay. */ - private setupExtensionMessageListeners() { + private setupExtensionListeners() { BrowserApi.messageListener("overlay.background", this.handleExtensionMessage); + BrowserApi.addListener(chrome.webNavigation.onCommitted, this.handleWebNavigationOnCommitted); BrowserApi.addListener(chrome.runtime.onConnect, this.handlePortOnConnect); } @@ -691,18 +2231,42 @@ class OverlayBackground implements OverlayBackgroundInterface { ) => { const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command]; if (!handler) { - return; + return null; } const messageResponse = handler({ message, sender }); - if (!messageResponse) { + if (typeof messageResponse === "undefined") { + return null; + } + + Promise.resolve(messageResponse) + .then((response) => sendResponse(response)) + .catch((error) => this.logService.error(error)); + return true; + }; + + /** + * Handles clearing page details and sub frame offsets when a frame or tab navigation event occurs. + * + * @param details - The details of the web navigation event + */ + private handleWebNavigationOnCommitted = ( + details: chrome.webNavigation.WebNavigationTransitionCallbackDetails, + ) => { + const { frameId, tabId } = details; + const subFrames = this.subFrameOffsetsForTab[tabId]; + if (frameId === 0) { + this.removePageDetails(tabId); + if (subFrames) { + subFrames.clear(); + delete this.subFrameOffsetsForTab[tabId]; + } return; } - // 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 - Promise.resolve(messageResponse).then((response) => sendResponse(response)); - return true; + if (subFrames && subFrames.has(frameId)) { + subFrames.delete(frameId); + } }; /** @@ -711,30 +2275,58 @@ class OverlayBackground implements OverlayBackgroundInterface { * @param port - The port that connected to the extension background */ private handlePortOnConnect = async (port: chrome.runtime.Port) => { - const isOverlayListPort = port.name === AutofillOverlayPort.List; - const isOverlayButtonPort = port.name === AutofillOverlayPort.Button; - if (!isOverlayListPort && !isOverlayButtonPort) { + const isInlineMenuListMessageConnector = port.name === AutofillOverlayPort.ListMessageConnector; + const isInlineMenuButtonMessageConnector = + port.name === AutofillOverlayPort.ButtonMessageConnector; + if (isInlineMenuListMessageConnector || isInlineMenuButtonMessageConnector) { + port.onMessage.addListener(this.handleOverlayElementPortMessage); return; } + const isInlineMenuListPort = port.name === AutofillOverlayPort.List; + const isInlineMenuButtonPort = port.name === AutofillOverlayPort.Button; + if (!isInlineMenuListPort && !isInlineMenuButtonPort) { + return; + } + + if (!this.portKeyForTab[port.sender.tab.id]) { + this.portKeyForTab[port.sender.tab.id] = generateRandomChars(12); + } + this.storeOverlayPort(port); + port.onDisconnect.addListener(this.handlePortOnDisconnect); port.onMessage.addListener(this.handleOverlayElementPortMessage); port.postMessage({ - command: `initAutofillOverlay${isOverlayListPort ? "List" : "Button"}`, + command: `initAutofillInlineMenu${isInlineMenuListPort ? "List" : "Button"}`, + iframeUrl: chrome.runtime.getURL( + `overlay/menu-${isInlineMenuListPort ? "list" : "button"}.html`, + ), + pageTitle: chrome.i18n.getMessage( + isInlineMenuListPort ? "bitwardenVault" : "bitwardenOverlayButton", + ), authStatus: await this.getAuthStatus(), - styleSheetUrl: chrome.runtime.getURL(`overlay/${isOverlayListPort ? "list" : "button"}.css`), + styleSheetUrl: chrome.runtime.getURL( + `overlay/menu-${isInlineMenuListPort ? "list" : "button"}.css`, + ), theme: await firstValueFrom(this.themeStateService.selectedTheme$), - translations: this.getTranslations(), - ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null, + translations: this.getInlineMenuTranslations(), + ciphers: isInlineMenuListPort ? await this.getInlineMenuCipherData() : null, + portKey: this.portKeyForTab[port.sender.tab.id], + portName: isInlineMenuListPort + ? AutofillOverlayPort.ListMessageConnector + : AutofillOverlayPort.ButtonMessageConnector, + filledByCipherType: this.focusedFieldData?.filledByCipherType, + showInlineMenuAccountCreation: this.showInlineMenuAccountCreation(), + showPasskeysLabels: this.showPasskeysLabelsWithinInlineMenu, }); - this.updateOverlayPosition( + this.updateInlineMenuPosition( { - overlayElement: isOverlayListPort + overlayElement: isInlineMenuListPort ? AutofillOverlayElement.List : AutofillOverlayElement.Button, }, port.sender, - ); + ).catch((error) => this.logService.error(error)); }; /** @@ -744,14 +2336,14 @@ class OverlayBackground implements OverlayBackgroundInterface { | */ private storeOverlayPort(port: chrome.runtime.Port) { if (port.name === AutofillOverlayPort.List) { - this.storeExpiredOverlayPort(this.overlayListPort); - this.overlayListPort = port; + this.storeExpiredOverlayPort(this.inlineMenuListPort); + this.inlineMenuListPort = port; return; } if (port.name === AutofillOverlayPort.Button) { - this.storeExpiredOverlayPort(this.overlayButtonPort); - this.overlayButtonPort = port; + this.storeExpiredOverlayPort(this.inlineMenuButtonPort); + this.inlineMenuButtonPort = port; } } @@ -778,15 +2370,20 @@ class OverlayBackground implements OverlayBackgroundInterface { message: OverlayBackgroundExtensionMessage, port: chrome.runtime.Port, ) => { - const command = message?.command; - let handler: CallableFunction | undefined; - - if (port.name === AutofillOverlayPort.Button) { - handler = this.overlayButtonPortMessageHandlers[command]; + const tabPortKey = this.portKeyForTab[port.sender.tab.id]; + if (!tabPortKey || tabPortKey !== message?.portKey) { + return; } - if (port.name === AutofillOverlayPort.List) { - handler = this.overlayListPortMessageHandlers[command]; + const command = message.command; + let handler: CallableFunction | undefined; + + if (port.name === AutofillOverlayPort.ButtonMessageConnector) { + handler = this.inlineMenuButtonPortMessageHandlers[command]; + } + + if (port.name === AutofillOverlayPort.ListMessageConnector) { + handler = this.inlineMenuListPortMessageHandlers[command]; } if (!handler) { @@ -795,6 +2392,24 @@ class OverlayBackground implements OverlayBackgroundInterface { handler({ message, port }); }; -} -export default OverlayBackground; + /** + * Ensures that the inline menu list and button port + * references are reset when they are disconnected. + * + * @param port - The port that was disconnected + */ + private handlePortOnDisconnect = (port: chrome.runtime.Port) => { + if (port.name === AutofillOverlayPort.List) { + this.inlineMenuListPort = null; + this.isInlineMenuListVisible = false; + this.inlineMenuPosition.list = null; + } + + if (port.name === AutofillOverlayPort.Button) { + this.inlineMenuButtonPort = null; + this.isInlineMenuButtonVisible = false; + this.inlineMenuPosition.button = null; + } + }; +} diff --git a/apps/browser/src/autofill/background/tabs.background.spec.ts b/apps/browser/src/autofill/background/tabs.background.spec.ts index b95e303f17e..4473eb452f3 100644 --- a/apps/browser/src/autofill/background/tabs.background.spec.ts +++ b/apps/browser/src/autofill/background/tabs.background.spec.ts @@ -11,7 +11,7 @@ import { } from "../spec/testing-utils"; import NotificationBackground from "./notification.background"; -import OverlayBackground from "./overlay.background"; +import { OverlayBackground } from "./overlay.background"; import TabsBackground from "./tabs.background"; describe("TabsBackground", () => { @@ -146,6 +146,7 @@ describe("TabsBackground", () => { beforeEach(() => { mainBackground.onUpdatedRan = false; + mainBackground.configService.getFeatureFlag = jest.fn().mockResolvedValue(true); tabsBackground["focusedWindowId"] = focusedWindowId; tab = mock({ windowId: focusedWindowId, @@ -154,18 +155,6 @@ describe("TabsBackground", () => { }); }); - it("removes the cached page details from the overlay background if the tab status is `loading`", () => { - triggerTabOnUpdatedEvent(focusedWindowId, { status: "loading" }, tab); - - expect(overlayBackground.removePageDetails).toHaveBeenCalledWith(focusedWindowId); - }); - - it("removes the cached page details from the overlay background if the tab status is `unloaded`", () => { - triggerTabOnUpdatedEvent(focusedWindowId, { status: "unloaded" }, tab); - - expect(overlayBackground.removePageDetails).toHaveBeenCalledWith(focusedWindowId); - }); - it("skips updating the current tab data the focusedWindowId is set to a value less than zero", async () => { tab.windowId = -1; triggerTabOnUpdatedEvent(focusedWindowId, { status: "loading" }, tab); diff --git a/apps/browser/src/autofill/background/tabs.background.ts b/apps/browser/src/autofill/background/tabs.background.ts index 53c801ff7bc..0513220c277 100644 --- a/apps/browser/src/autofill/background/tabs.background.ts +++ b/apps/browser/src/autofill/background/tabs.background.ts @@ -1,7 +1,9 @@ +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; + import MainBackground from "../../background/main.background"; +import { OverlayBackground } from "./abstractions/overlay.background"; import NotificationBackground from "./notification.background"; -import OverlayBackground from "./overlay.background"; export default class TabsBackground { constructor( @@ -86,8 +88,11 @@ export default class TabsBackground { changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab, ) => { + const overlayImprovementsFlag = await this.main.configService.getFeatureFlag( + FeatureFlag.InlineMenuPositioningImprovements, + ); const removePageDetailsStatus = new Set(["loading", "unloaded"]); - if (removePageDetailsStatus.has(changeInfo.status)) { + if (!!overlayImprovementsFlag && removePageDetailsStatus.has(changeInfo.status)) { this.overlayBackground.removePageDetails(tabId); } @@ -99,7 +104,7 @@ export default class TabsBackground { return; } - await this.overlayBackground.updateOverlayCiphers(); + await this.overlayBackground.updateOverlayCiphers(false); if (this.main.onUpdatedRan) { return; @@ -129,7 +134,7 @@ export default class TabsBackground { await Promise.all([ this.main.refreshBadge(), this.main.refreshMenu(), - this.overlayBackground.updateOverlayCiphers(), + this.overlayBackground.updateOverlayCiphers(false), ]); }; } diff --git a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts index cb1c59dca59..c1567b46cd9 100644 --- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts +++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts @@ -16,6 +16,7 @@ import { CREATE_CARD_ID, CREATE_IDENTITY_ID, CREATE_LOGIN_ID, + ExtensionCommand, GENERATE_PASSWORD_ID, NOOP_COMMAND_SUFFIX, } from "@bitwarden/common/autofill/constants"; @@ -79,7 +80,7 @@ export class ContextMenuClickedHandler { if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) { const retryMessage: LockedVaultPendingNotificationsData = { commandToRetry: { - message: { command: NOOP_COMMAND_SUFFIX, contextMenuOnClickData: info }, + message: { command: ExtensionCommand.NoopCommand, contextMenuOnClickData: info }, sender: { tab: tab }, }, target: "contextmenus.background", diff --git a/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts b/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts index 67637da2fdd..21eadfaf668 100644 --- a/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts +++ b/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts @@ -6,16 +6,15 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service"; - import { MainContextMenuHandler } from "./main-context-menu-handler"; describe("context-menu", () => { - let stateService: MockProxy; + let stateService: MockProxy; let autofillSettingsService: MockProxy; let i18nService: MockProxy; let logService: MockProxy; diff --git a/apps/browser/src/autofill/browser/main-context-menu-handler.ts b/apps/browser/src/autofill/browser/main-context-menu-handler.ts index a02a3a84d4b..7b074a566a2 100644 --- a/apps/browser/src/autofill/browser/main-context-menu-handler.ts +++ b/apps/browser/src/autofill/browser/main-context-menu-handler.ts @@ -20,12 +20,11 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service"; - import { InitContextMenuItems } from "./abstractions/main-context-menu-handler"; export class MainContextMenuHandler { @@ -143,7 +142,7 @@ export class MainContextMenuHandler { ]; constructor( - private stateService: BrowserStateService, + private stateService: StateService, private autofillSettingsService: AutofillSettingsServiceAbstraction, private i18nService: I18nService, private logService: LogService, diff --git a/apps/browser/src/autofill/clipboard/clear-clipboard.ts b/apps/browser/src/autofill/clipboard/clear-clipboard.ts index f8018bb036a..426d6539513 100644 --- a/apps/browser/src/autofill/clipboard/clear-clipboard.ts +++ b/apps/browser/src/autofill/clipboard/clear-clipboard.ts @@ -1,11 +1,9 @@ import { BrowserApi } from "../../platform/browser/browser-api"; -export const clearClipboardAlarmName = "clearClipboard"; - export class ClearClipboard { /** We currently rely on an active tab with an injected content script (`../content/misc-utils.ts`) to clear the clipboard via `window.navigator.clipboard.writeText(text)` - + With https://bugs.chromium.org/p/chromium/issues/detail?id=1160302 it was said that service workers, would have access to the clipboard api and then we could migrate to a simpler solution */ diff --git a/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.spec.ts b/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.spec.ts index 522da229244..d0d42cc06f7 100644 --- a/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.spec.ts +++ b/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.spec.ts @@ -1,30 +1,45 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { firstValueFrom, Subscription } from "rxjs"; import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { setAlarmTime } from "../../platform/alarms/alarm-state"; import { BrowserApi } from "../../platform/browser/browser-api"; +import { BrowserTaskSchedulerService } from "../../platform/services/abstractions/browser-task-scheduler.service"; -import { clearClipboardAlarmName } from "./clear-clipboard"; +import { ClearClipboard } from "./clear-clipboard"; import { GeneratePasswordToClipboardCommand } from "./generate-password-to-clipboard-command"; -jest.mock("../../platform/alarms/alarm-state", () => { +jest.mock("rxjs", () => { + const actual = jest.requireActual("rxjs"); return { - setAlarmTime: jest.fn(), + ...actual, + firstValueFrom: jest.fn(), }; }); -const setAlarmTimeMock = setAlarmTime as jest.Mock; - describe("GeneratePasswordToClipboardCommand", () => { let passwordGenerationService: MockProxy; let autofillSettingsService: MockProxy; + let browserTaskSchedulerService: MockProxy; let sut: GeneratePasswordToClipboardCommand; beforeEach(() => { passwordGenerationService = mock(); + autofillSettingsService = mock(); + browserTaskSchedulerService = mock({ + setTimeout: jest.fn((taskName, timeoutInMs) => { + const timeoutHandle = setTimeout(() => { + if (taskName === ScheduledTaskNames.generatePasswordClearClipboardTimeout) { + void ClearClipboard.run(); + } + }, timeoutInMs); + + return new Subscription(() => clearTimeout(timeoutHandle)); + }), + }); passwordGenerationService.getOptions.mockResolvedValue([{ length: 8 }, {} as any]); @@ -35,6 +50,7 @@ describe("GeneratePasswordToClipboardCommand", () => { sut = new GeneratePasswordToClipboardCommand( passwordGenerationService, autofillSettingsService, + browserTaskSchedulerService, ); }); @@ -44,20 +60,24 @@ describe("GeneratePasswordToClipboardCommand", () => { describe("generatePasswordToClipboard", () => { it("has clear clipboard value", async () => { - jest.spyOn(sut as any, "getClearClipboard").mockImplementation(() => 5 * 60); // 5 minutes + jest.useFakeTimers(); + jest.spyOn(ClearClipboard, "run"); + (firstValueFrom as jest.Mock).mockResolvedValue(2 * 60); // 2 minutes await sut.generatePasswordToClipboard({ id: 1 } as any); + jest.advanceTimersByTime(2 * 60 * 1000); expect(jest.spyOn(BrowserApi, "sendTabsMessage")).toHaveBeenCalledTimes(1); - expect(jest.spyOn(BrowserApi, "sendTabsMessage")).toHaveBeenCalledWith(1, { command: "copyText", text: "PASSWORD", }); - - expect(setAlarmTimeMock).toHaveBeenCalledTimes(1); - - expect(setAlarmTimeMock).toHaveBeenCalledWith(clearClipboardAlarmName, expect.any(Number)); + expect(browserTaskSchedulerService.setTimeout).toHaveBeenCalledTimes(1); + expect(browserTaskSchedulerService.setTimeout).toHaveBeenCalledWith( + ScheduledTaskNames.generatePasswordClearClipboardTimeout, + expect.any(Number), + ); + expect(ClearClipboard.run).toHaveBeenCalledTimes(1); }); it("does not have clear clipboard value", async () => { @@ -71,8 +91,7 @@ describe("GeneratePasswordToClipboardCommand", () => { command: "copyText", text: "PASSWORD", }); - - expect(setAlarmTimeMock).not.toHaveBeenCalled(); + expect(browserTaskSchedulerService.setTimeout).not.toHaveBeenCalled(); }); }); }); diff --git a/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.ts b/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.ts index dadd61fbd12..cf3bc311aea 100644 --- a/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.ts +++ b/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.ts @@ -1,18 +1,25 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, Subscription } from "rxjs"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; -import { setAlarmTime } from "../../platform/alarms/alarm-state"; - -import { clearClipboardAlarmName } from "./clear-clipboard"; +import { ClearClipboard } from "./clear-clipboard"; import { copyToClipboard } from "./copy-to-clipboard-command"; export class GeneratePasswordToClipboardCommand { + private clearClipboardSubscription: Subscription; + constructor( private passwordGenerationService: PasswordGenerationServiceAbstraction, private autofillSettingsService: AutofillSettingsServiceAbstraction, - ) {} + private taskSchedulerService: TaskSchedulerService, + ) { + this.taskSchedulerService.registerTaskHandler( + ScheduledTaskNames.generatePasswordClearClipboardTimeout, + () => ClearClipboard.run(), + ); + } async getClearClipboard() { return await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$); @@ -22,14 +29,18 @@ export class GeneratePasswordToClipboardCommand { const [options] = await this.passwordGenerationService.getOptions(); const password = await this.passwordGenerationService.generatePassword(options); - // 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 - copyToClipboard(tab, password); + await copyToClipboard(tab, password); - const clearClipboard = await this.getClearClipboard(); - - if (clearClipboard != null) { - await setAlarmTime(clearClipboardAlarmName, clearClipboard * 1000); + const clearClipboardDelayInSeconds = await this.getClearClipboard(); + if (!clearClipboardDelayInSeconds) { + return; } + + const timeoutInMs = clearClipboardDelayInSeconds * 1000; + this.clearClipboardSubscription?.unsubscribe(); + this.clearClipboardSubscription = this.taskSchedulerService.setTimeout( + ScheduledTaskNames.generatePasswordClearClipboardTimeout, + timeoutInMs, + ); } } diff --git a/apps/browser/src/autofill/content/abstractions/autofill-init.ts b/apps/browser/src/autofill/content/abstractions/autofill-init.ts index 91866ffa0bb..ba815a0f29a 100644 --- a/apps/browser/src/autofill/content/abstractions/autofill-init.ts +++ b/apps/browser/src/autofill/content/abstractions/autofill-init.ts @@ -1,46 +1,42 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { AutofillOverlayElementType } from "../../enums/autofill-overlay.enum"; import AutofillScript from "../../models/autofill-script"; -type AutofillExtensionMessage = { +export type AutofillExtensionMessage = { command: string; tab?: chrome.tabs.Tab; sender?: string; fillScript?: AutofillScript; url?: string; + subFrameUrl?: string; + subFrameId?: string; pageDetailsUrl?: string; ciphers?: any; + isInlineMenuHidden?: boolean; + overlayElement?: AutofillOverlayElementType; + isFocusingFieldElement?: boolean; + authStatus?: AuthenticationStatus; + isOpeningFullInlineMenu?: boolean; + addNewCipherType?: CipherType; data?: { - authStatus?: AuthenticationStatus; - isFocusingFieldElement?: boolean; - isOverlayCiphersPopulated?: boolean; - direction?: "previous" | "next"; - isOpeningFullOverlay?: boolean; - forceCloseOverlay?: boolean; - autofillOverlayVisibility?: number; + direction?: "previous" | "next" | "current"; + forceCloseInlineMenu?: boolean; + inlineMenuVisibility?: number; }; }; -type AutofillExtensionMessageParam = { message: AutofillExtensionMessage }; +export type AutofillExtensionMessageParam = { message: AutofillExtensionMessage }; -type AutofillExtensionMessageHandlers = { +export type AutofillExtensionMessageHandlers = { [key: string]: CallableFunction; collectPageDetails: ({ message }: AutofillExtensionMessageParam) => void; collectPageDetailsImmediately: ({ message }: AutofillExtensionMessageParam) => void; fillForm: ({ message }: AutofillExtensionMessageParam) => void; - openAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void; - closeAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void; - addNewVaultItemFromOverlay: () => void; - redirectOverlayFocusOut: ({ message }: AutofillExtensionMessageParam) => void; - updateIsOverlayCiphersPopulated: ({ message }: AutofillExtensionMessageParam) => void; - bgUnlockPopoutOpened: () => void; - bgVaultItemRepromptPopoutOpened: () => void; - updateAutofillOverlayVisibility: ({ message }: AutofillExtensionMessageParam) => void; }; -interface AutofillInit { +export interface AutofillInit { init(): void; destroy(): void; } - -export { AutofillExtensionMessage, AutofillExtensionMessageHandlers, AutofillInit }; 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..98caee3d363 --- /dev/null +++ b/apps/browser/src/autofill/content/auto-submit-login.spec.ts @@ -0,0 +1,339 @@ +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/dom-query.service", () => { + const module = jest.requireActual("../services/dom-query.service"); + return { + DomQueryService: class extends module.DomQueryService { + deepQueryElements(element: HTMLElement, queryString: string): T[] { + return Array.from(element.querySelectorAll(queryString)) as T[]; + } + }, + }; +}); +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; + } + + 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, + }, + expect.any(Function), + ); + }); + + 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, + }, + expect.any(Function), + ); + }); + + 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..ab7f09f804d --- /dev/null +++ b/apps/browser/src/autofill/content/auto-submit-login.ts @@ -0,0 +1,327 @@ +import { EVENTS } from "@bitwarden/common/autofill/constants"; + +import AutofillPageDetails from "../models/autofill-page-details"; +import AutofillScript from "../models/autofill-script"; +import { SubmitLoginButtonNames } from "../services/autofill-constants"; +import { CollectAutofillContentService } from "../services/collect-autofill-content.service"; +import DomElementVisibilityService from "../services/dom-element-visibility.service"; +import { DomQueryService } from "../services/dom-query.service"; +import InsertAutofillContentService from "../services/insert-autofill-content.service"; +import { + elementIsInputElement, + getSubmitButtonKeywordsSet, + nodeIsFormElement, + sendExtensionMessage, +} from "../utils"; + +(function (globalContext) { + const domQueryService = new DomQueryService(); + const domElementVisibilityService = new DomElementVisibilityService(); + const collectAutofillContentService = new CollectAutofillContentService( + domElementVisibilityService, + domQueryService, + ); + const insertAutofillContentService = new InsertAutofillContentService( + domElementVisibilityService, + collectAutofillContentService, + ); + 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 = querySubmitButtonElement(element, "[type='submit']"); + if (genericSubmitElement) { + clickSubmitElement(genericSubmitElement, lastFieldIsPasswordInput); + return true; + } + + const buttonElement = querySubmitButtonElement(element, "button, [type='button']"); + if (buttonElement) { + clickSubmitElement(buttonElement, lastFieldIsPasswordInput); + return true; + } + + return false; + } + + /** + * Queries the element for a submit button element. If an element is found and has keywords + * that indicate a login action, the element is returned. + * + * @param element - The element to query for submit buttons + * @param selector - The selector to query for submit buttons + */ + function querySubmitButtonElement(element: HTMLElement, selector: string) { + const submitButtonElements = domQueryService.deepQueryElements( + element, + selector, + ); + for (let index = 0; index < submitButtonElements.length; index++) { + const submitElement = submitButtonElements[index]; + if (isLoginButton(submitElement)) { + return submitElement; + } + } + } + + /** + * 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 keywordsSet = getSubmitButtonKeywordsSet(element); + const keywordValues = Array.from(keywordsSet).join(","); + + return SubmitLoginButtonNames.some((keyword) => keywordValues.indexOf(keyword) > -1); + } + + /** + * 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[] = []; + domQueryService.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.spec.ts b/apps/browser/src/autofill/content/autofill-init.spec.ts index 302b520e336..ebfbda75b56 100644 --- a/apps/browser/src/autofill/content/autofill-init.spec.ts +++ b/apps/browser/src/autofill/content/autofill-init.spec.ts @@ -1,26 +1,29 @@ -import { mock } from "jest-mock-extended"; - -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; +import { mock, MockProxy } from "jest-mock-extended"; import AutofillPageDetails from "../models/autofill-page-details"; import AutofillScript from "../models/autofill-script"; -import AutofillOverlayContentService from "../services/autofill-overlay-content.service"; +import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service"; +import { OverlayNotificationsContentService } from "../overlay/notifications/abstractions/overlay-notifications-content.service"; +import { DomQueryService } from "../services/abstractions/dom-query.service"; +import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; import { flushPromises, mockQuerySelectorAllDefinedCall, sendMockExtensionMessage, } from "../spec/testing-utils"; -import { RedirectFocusDirection } from "../utils/autofill-overlay.enum"; import { AutofillExtensionMessage } from "./abstractions/autofill-init"; import AutofillInit from "./autofill-init"; describe("AutofillInit", () => { + let domQueryService: MockProxy; + let overlayNotificationsContentService: MockProxy; + let inlineMenuElements: MockProxy; + let autofillOverlayContentService: MockProxy; let autofillInit: AutofillInit; - const autofillOverlayContentService = mock(); const originalDocumentReadyState = document.readyState; const mockQuerySelectorAll = mockQuerySelectorAllDefinedCall(); + let sendExtensionMessageSpy: jest.SpyInstance; beforeEach(() => { chrome.runtime.connect = jest.fn().mockReturnValue({ @@ -28,7 +31,19 @@ describe("AutofillInit", () => { addListener: jest.fn(), }, }); - autofillInit = new AutofillInit(autofillOverlayContentService); + domQueryService = mock(); + overlayNotificationsContentService = mock(); + inlineMenuElements = mock(); + autofillOverlayContentService = mock(); + autofillInit = new AutofillInit( + domQueryService, + autofillOverlayContentService, + inlineMenuElements, + overlayNotificationsContentService, + ); + sendExtensionMessageSpy = jest + .spyOn(autofillInit as any, "sendExtensionMessage") + .mockImplementation(); window.IntersectionObserver = jest.fn(() => mock()); }); @@ -61,13 +76,9 @@ describe("AutofillInit", () => { autofillInit.init(); jest.advanceTimersByTime(250); - expect(chrome.runtime.sendMessage).toHaveBeenCalledWith( - { - command: "bgCollectPageDetails", - sender: "autofillInit", - }, - expect.any(Function), - ); + expect(sendExtensionMessageSpy).toHaveBeenCalledWith("bgCollectPageDetails", { + sender: "autofillInit", + }); }); it("registers a window load listener to collect the page details if the document is not in a `complete` ready state", () => { @@ -106,15 +117,15 @@ describe("AutofillInit", () => { sender = mock(); }); - it("returns a undefined value if a extension message handler is not found with the given message command", () => { + it("returns a null value if a extension message handler is not found with the given message command", () => { message.command = "unknownCommand"; const response = autofillInit["handleExtensionMessage"](message, sender, sendResponse); - expect(response).toBe(undefined); + expect(response).toBe(null); }); - it("returns a undefined value if the message handler does not return a response", async () => { + it("returns a null value if the message handler does not return a response", async () => { const response1 = await autofillInit["handleExtensionMessage"](message, sender, sendResponse); await flushPromises(); @@ -126,7 +137,7 @@ describe("AutofillInit", () => { const response2 = autofillInit["handleExtensionMessage"](message, sender, sendResponse); await flushPromises(); - expect(response2).toBe(undefined); + expect(response2).toBe(null); }); it("returns a true value and calls sendResponse if the message handler returns a response", async () => { @@ -155,6 +166,32 @@ describe("AutofillInit", () => { autofillInit.init(); }); + it("triggers extension message handlers from the AutofillOverlayContentService", () => { + autofillOverlayContentService.messageHandlers.messageHandler = jest.fn(); + + sendMockExtensionMessage({ command: "messageHandler" }, sender, sendResponse); + + expect(autofillOverlayContentService.messageHandlers.messageHandler).toHaveBeenCalled(); + }); + + it("triggers extension message handlers from the AutofillInlineMenuContentService", () => { + inlineMenuElements.messageHandlers.messageHandler = jest.fn(); + + sendMockExtensionMessage({ command: "messageHandler" }, sender, sendResponse); + + expect(inlineMenuElements.messageHandlers.messageHandler).toHaveBeenCalled(); + }); + + it("triggers extension message handlers from the OverlayNotificationsContentService", () => { + overlayNotificationsContentService.messageHandlers.messageHandler = jest.fn(); + + sendMockExtensionMessage({ command: "messageHandler" }, sender, sendResponse); + + expect( + overlayNotificationsContentService.messageHandlers.messageHandler, + ).toHaveBeenCalled(); + }); + describe("collectPageDetails", () => { it("sends the collected page details for autofill using a background script message", async () => { const pageDetails: AutofillPageDetails = { @@ -177,8 +214,7 @@ describe("AutofillInit", () => { sendMockExtensionMessage(message, sender, sendResponse); await flushPromises(); - expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ - command: "collectPageDetailsResponse", + expect(sendExtensionMessageSpy).toHaveBeenCalledWith("collectPageDetailsResponse", { tab: message.tab, details: pageDetails, sender: message.sender, @@ -226,14 +262,11 @@ describe("AutofillInit", () => { }); it("skips calling the InsertAutofillContentService and does not fill the form if the url to fill is not equal to the current tab url", async () => { - const fillScript = mock(); - const message = { + sendMockExtensionMessage({ command: "fillForm", fillScript, pageDetailsUrl: "https://a-different-url.com", - }; - - sendMockExtensionMessage(message); + }); await flushPromises(); expect(autofillInit["insertAutofillContentService"].fillForm).not.toHaveBeenCalledWith( @@ -255,7 +288,10 @@ describe("AutofillInit", () => { }); it("removes the overlay when filling the form", async () => { - const blurAndRemoveOverlaySpy = jest.spyOn(autofillInit as any, "blurAndRemoveOverlay"); + const blurAndRemoveOverlaySpy = jest.spyOn( + autofillInit as any, + "blurFocusedFieldAndCloseInlineMenu", + ); sendMockExtensionMessage({ command: "fillForm", fillScript, @@ -268,10 +304,6 @@ describe("AutofillInit", () => { it("updates the isCurrentlyFilling property of the overlay to true after filling", async () => { jest.useFakeTimers(); - jest.spyOn(autofillInit as any, "updateOverlayIsCurrentlyFilling"); - jest - .spyOn(autofillInit["autofillOverlayContentService"], "focusMostRecentOverlayField") - .mockImplementation(); sendMockExtensionMessage({ command: "fillForm", @@ -281,292 +313,18 @@ describe("AutofillInit", () => { await flushPromises(); jest.advanceTimersByTime(300); - expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(1, true); + expect(sendExtensionMessageSpy).toHaveBeenNthCalledWith( + 1, + "updateIsFieldCurrentlyFilling", + { isFieldCurrentlyFilling: true }, + ); expect(autofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( fillScript, ); - expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(2, false); - }); - - it("skips attempting to focus the most recent field if the autofillOverlayContentService is not present", async () => { - jest.useFakeTimers(); - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "updateOverlayIsCurrentlyFilling"); - jest - .spyOn(newAutofillInit["insertAutofillContentService"], "fillForm") - .mockImplementation(); - - sendMockExtensionMessage({ - command: "fillForm", - fillScript, - pageDetailsUrl: window.location.href, - }); - await flushPromises(); - jest.advanceTimersByTime(300); - - expect(newAutofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith( - 1, - true, - ); - expect(newAutofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( - fillScript, - ); - expect(newAutofillInit["updateOverlayIsCurrentlyFilling"]).not.toHaveBeenNthCalledWith( + expect(sendExtensionMessageSpy).toHaveBeenNthCalledWith( 2, - false, - ); - }); - }); - - describe("openAutofillOverlay", () => { - const message = { - command: "openAutofillOverlay", - data: { - isFocusingFieldElement: true, - isOpeningFullOverlay: true, - authStatus: AuthenticationStatus.Unlocked, - }, - }; - - it("skips attempting to open the autofill overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage(message); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("opens the autofill overlay", () => { - sendMockExtensionMessage(message); - - expect( - autofillInit["autofillOverlayContentService"].openAutofillOverlay, - ).toHaveBeenCalledWith({ - isFocusingFieldElement: message.data.isFocusingFieldElement, - isOpeningFullOverlay: message.data.isOpeningFullOverlay, - authStatus: message.data.authStatus, - }); - }); - }); - - describe("closeAutofillOverlay", () => { - beforeEach(() => { - autofillInit["autofillOverlayContentService"].isFieldCurrentlyFocused = false; - autofillInit["autofillOverlayContentService"].isCurrentlyFilling = false; - }); - - it("skips attempting to remove the overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ - command: "closeAutofillOverlay", - data: { forceCloseOverlay: false }, - }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("removes the autofill overlay if the message flags a forced closure", () => { - sendMockExtensionMessage({ - command: "closeAutofillOverlay", - data: { forceCloseOverlay: true }, - }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).toHaveBeenCalled(); - }); - - it("ignores the message if a field is currently focused", () => { - autofillInit["autofillOverlayContentService"].isFieldCurrentlyFocused = true; - - sendMockExtensionMessage({ command: "closeAutofillOverlay" }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, - ).not.toHaveBeenCalled(); - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).not.toHaveBeenCalled(); - }); - - it("removes the autofill overlay list if the overlay is currently filling", () => { - autofillInit["autofillOverlayContentService"].isCurrentlyFilling = true; - - sendMockExtensionMessage({ command: "closeAutofillOverlay" }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, - ).toHaveBeenCalled(); - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).not.toHaveBeenCalled(); - }); - - it("removes the entire overlay if the overlay is not currently filling", () => { - sendMockExtensionMessage({ command: "closeAutofillOverlay" }); - - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, - ).not.toHaveBeenCalled(); - expect( - autofillInit["autofillOverlayContentService"].removeAutofillOverlay, - ).toHaveBeenCalled(); - }); - }); - - describe("addNewVaultItemFromOverlay", () => { - it("will not add a new vault item if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("will add a new vault item", () => { - sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" }); - - expect(autofillInit["autofillOverlayContentService"].addNewVaultItem).toHaveBeenCalled(); - }); - }); - - describe("redirectOverlayFocusOut", () => { - const message = { - command: "redirectOverlayFocusOut", - data: { - direction: RedirectFocusDirection.Next, - }, - }; - - it("ignores the message to redirect focus if the autofillOverlayContentService does not exist", () => { - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage(message); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("redirects the overlay focus", () => { - sendMockExtensionMessage(message); - - expect( - autofillInit["autofillOverlayContentService"].redirectOverlayFocusOut, - ).toHaveBeenCalledWith(message.data.direction); - }); - }); - - describe("updateIsOverlayCiphersPopulated", () => { - const message = { - command: "updateIsOverlayCiphersPopulated", - data: { - isOverlayCiphersPopulated: true, - }, - }; - - it("skips updating whether the ciphers are populated if the autofillOverlayContentService does note exist", () => { - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - - sendMockExtensionMessage(message); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - }); - - it("updates whether the overlay ciphers are populated", () => { - sendMockExtensionMessage(message); - - expect(autofillInit["autofillOverlayContentService"].isOverlayCiphersPopulated).toEqual( - message.data.isOverlayCiphersPopulated, - ); - }); - }); - - describe("bgUnlockPopoutOpened", () => { - it("skips attempting to blur and remove the overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgUnlockPopoutOpened" }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - expect(newAutofillInit["removeAutofillOverlay"]).not.toHaveBeenCalled(); - }); - - it("blurs the most recently focused feel and remove the autofill overlay", () => { - jest.spyOn(autofillInit["autofillOverlayContentService"], "blurMostRecentOverlayField"); - jest.spyOn(autofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgUnlockPopoutOpened" }); - - expect( - autofillInit["autofillOverlayContentService"].blurMostRecentOverlayField, - ).toHaveBeenCalled(); - expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled(); - }); - }); - - describe("bgVaultItemRepromptPopoutOpened", () => { - it("skips attempting to blur and remove the overlay if the autofillOverlayContentService is not present", () => { - const newAutofillInit = new AutofillInit(undefined); - newAutofillInit.init(); - jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgVaultItemRepromptPopoutOpened" }); - - expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); - expect(newAutofillInit["removeAutofillOverlay"]).not.toHaveBeenCalled(); - }); - - it("blurs the most recently focused feel and remove the autofill overlay", () => { - jest.spyOn(autofillInit["autofillOverlayContentService"], "blurMostRecentOverlayField"); - jest.spyOn(autofillInit as any, "removeAutofillOverlay"); - - sendMockExtensionMessage({ command: "bgVaultItemRepromptPopoutOpened" }); - - expect( - autofillInit["autofillOverlayContentService"].blurMostRecentOverlayField, - ).toHaveBeenCalled(); - expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled(); - }); - }); - - describe("updateAutofillOverlayVisibility", () => { - beforeEach(() => { - autofillInit["autofillOverlayContentService"].autofillOverlayVisibility = - AutofillOverlayVisibility.OnButtonClick; - }); - - it("skips attempting to update the overlay visibility if the autofillOverlayVisibility data value is not present", () => { - sendMockExtensionMessage({ - command: "updateAutofillOverlayVisibility", - data: {}, - }); - - expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual( - AutofillOverlayVisibility.OnButtonClick, - ); - }); - - it("updates the overlay visibility value", () => { - const message = { - command: "updateAutofillOverlayVisibility", - data: { - autofillOverlayVisibility: AutofillOverlayVisibility.Off, - }, - }; - - sendMockExtensionMessage(message); - - expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual( - message.data.autofillOverlayVisibility, + "updateIsFieldCurrentlyFilling", + { isFieldCurrentlyFilling: false }, ); }); }); diff --git a/apps/browser/src/autofill/content/autofill-init.ts b/apps/browser/src/autofill/content/autofill-init.ts index e78a1fb5ee1..c0cbac3ae67 100644 --- a/apps/browser/src/autofill/content/autofill-init.ts +++ b/apps/browser/src/autofill/content/autofill-init.ts @@ -1,6 +1,11 @@ +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 { OverlayNotificationsContentService } from "../overlay/notifications/abstractions/overlay-notifications-content.service"; import { AutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service"; -import CollectAutofillContentService from "../services/collect-autofill-content.service"; +import { DomQueryService } from "../services/abstractions/dom-query.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"; @@ -12,7 +17,7 @@ import { } from "./abstractions/autofill-init"; class AutofillInit implements AutofillInitInterface { - private readonly autofillOverlayContentService: AutofillOverlayContentService | undefined; + private readonly sendExtensionMessage = sendExtensionMessage; private readonly domElementVisibilityService: DomElementVisibilityService; private readonly collectAutofillContentService: CollectAutofillContentService; private readonly insertAutofillContentService: InsertAutofillContentService; @@ -21,27 +26,29 @@ class AutofillInit implements AutofillInitInterface { collectPageDetails: ({ message }) => this.collectPageDetails(message), collectPageDetailsImmediately: ({ message }) => this.collectPageDetails(message, true), fillForm: ({ message }) => this.fillForm(message), - openAutofillOverlay: ({ message }) => this.openAutofillOverlay(message), - closeAutofillOverlay: ({ message }) => this.removeAutofillOverlay(message), - addNewVaultItemFromOverlay: () => this.addNewVaultItemFromOverlay(), - redirectOverlayFocusOut: ({ message }) => this.redirectOverlayFocusOut(message), - updateIsOverlayCiphersPopulated: ({ message }) => this.updateIsOverlayCiphersPopulated(message), - bgUnlockPopoutOpened: () => this.blurAndRemoveOverlay(), - bgVaultItemRepromptPopoutOpened: () => this.blurAndRemoveOverlay(), - updateAutofillOverlayVisibility: ({ message }) => this.updateAutofillOverlayVisibility(message), }; /** * AutofillInit constructor. Initializes the DomElementVisibilityService, * CollectAutofillContentService and InsertAutofillContentService classes. * + * @param domQueryService - Service used to handle DOM queries. * @param autofillOverlayContentService - The autofill overlay content service, potentially undefined. + * @param autofillInlineMenuContentService - The inline menu content service, potentially undefined. + * @param overlayNotificationsContentService - The overlay notifications content service, potentially undefined. */ - constructor(autofillOverlayContentService?: AutofillOverlayContentService) { - this.autofillOverlayContentService = autofillOverlayContentService; - this.domElementVisibilityService = new DomElementVisibilityService(); + constructor( + private domQueryService: DomQueryService, + private autofillOverlayContentService?: AutofillOverlayContentService, + private autofillInlineMenuContentService?: AutofillInlineMenuContentService, + private overlayNotificationsContentService?: OverlayNotificationsContentService, + ) { + this.domElementVisibilityService = new DomElementVisibilityService( + this.autofillInlineMenuContentService, + ); this.collectAutofillContentService = new CollectAutofillContentService( this.domElementVisibilityService, + domQueryService, this.autofillOverlayContentService, ); this.insertAutofillContentService = new InsertAutofillContentService( @@ -70,7 +77,7 @@ class AutofillInit implements AutofillInitInterface { const sendCollectDetailsMessage = () => { this.clearCollectPageDetailsOnLoadTimeout(); this.collectPageDetailsOnLoadTimeout = setTimeout( - () => sendExtensionMessage("bgCollectPageDetails", { sender: "autofillInit" }), + () => this.sendExtensionMessage("bgCollectPageDetails", { sender: "autofillInit" }), 250, ); }; @@ -79,7 +86,7 @@ class AutofillInit implements AutofillInitInterface { sendCollectDetailsMessage(); } - globalThis.addEventListener("load", sendCollectDetailsMessage); + globalThis.addEventListener(EVENTS.LOAD, sendCollectDetailsMessage); } /** @@ -102,8 +109,7 @@ class AutofillInit implements AutofillInitInterface { return pageDetails; } - void chrome.runtime.sendMessage({ - command: "collectPageDetailsResponse", + void this.sendExtensionMessage("collectPageDetailsResponse", { tab: message.tab, details: pageDetails, sender: message.sender, @@ -120,134 +126,28 @@ class AutofillInit implements AutofillInitInterface { return; } - this.blurAndRemoveOverlay(); - this.updateOverlayIsCurrentlyFilling(true); + this.blurFocusedFieldAndCloseInlineMenu(); + await this.sendExtensionMessage("updateIsFieldCurrentlyFilling", { + isFieldCurrentlyFilling: true, + }); await this.insertAutofillContentService.fillForm(fillScript); - if (!this.autofillOverlayContentService) { - return; - } - - setTimeout(() => this.updateOverlayIsCurrentlyFilling(false), 250); - } - - /** - * Handles updating the overlay is currently filling value. - * - * @param isCurrentlyFilling - Indicates if the overlay is currently filling - */ - private updateOverlayIsCurrentlyFilling(isCurrentlyFilling: boolean) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.isCurrentlyFilling = isCurrentlyFilling; - } - - /** - * Opens the autofill overlay. - * - * @param data - The extension message data. - */ - private openAutofillOverlay({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.openAutofillOverlay(data); - } - - /** - * Blurs the most recent overlay field and removes the overlay. Used - * in cases where the background unlock or vault item reprompt popout - * is opened. - */ - private blurAndRemoveOverlay() { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.blurMostRecentOverlayField(); - this.removeAutofillOverlay(); - } - - /** - * Removes the autofill overlay if the field is not currently focused. - * If the autofill is currently filling, only the overlay list will be - * removed. - */ - private removeAutofillOverlay(message?: AutofillExtensionMessage) { - if (message?.data?.forceCloseOverlay) { - this.autofillOverlayContentService?.removeAutofillOverlay(); - return; - } - - if ( - !this.autofillOverlayContentService || - this.autofillOverlayContentService.isFieldCurrentlyFocused - ) { - return; - } - - if (this.autofillOverlayContentService.isCurrentlyFilling) { - this.autofillOverlayContentService.removeAutofillOverlayList(); - return; - } - - this.autofillOverlayContentService.removeAutofillOverlay(); - } - - /** - * Adds a new vault item from the overlay. - */ - private addNewVaultItemFromOverlay() { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.addNewVaultItem(); - } - - /** - * Redirects the overlay focus out of an overlay iframe. - * - * @param data - Contains the direction to redirect the focus. - */ - private redirectOverlayFocusOut({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.redirectOverlayFocusOut(data?.direction); - } - - /** - * Updates whether the current tab has ciphers that can populate the overlay list - * - * @param data - Contains the isOverlayCiphersPopulated value - * - */ - private updateIsOverlayCiphersPopulated({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService) { - return; - } - - this.autofillOverlayContentService.isOverlayCiphersPopulated = Boolean( - data?.isOverlayCiphersPopulated, + setTimeout( + () => + this.sendExtensionMessage("updateIsFieldCurrentlyFilling", { + isFieldCurrentlyFilling: false, + }), + 250, ); } /** - * Updates the autofill overlay visibility. - * - * @param data - Contains the autoFillOverlayVisibility value + * Blurs the most recently focused field and removes the inline menu. Used + * in cases where the background unlock or vault item reprompt popout + * is opened. */ - private updateAutofillOverlayVisibility({ data }: AutofillExtensionMessage) { - if (!this.autofillOverlayContentService || isNaN(data?.autofillOverlayVisibility)) { - return; - } - - this.autofillOverlayContentService.autofillOverlayVisibility = data?.autofillOverlayVisibility; + private blurFocusedFieldAndCloseInlineMenu() { + this.autofillOverlayContentService?.blurMostRecentlyFocusedField(true); } /** @@ -279,22 +179,41 @@ class AutofillInit implements AutofillInitInterface { sendResponse: (response?: any) => void, ): boolean => { const command: string = message.command; - const handler: CallableFunction | undefined = this.extensionMessageHandlers[command]; + const handler: CallableFunction | undefined = this.getExtensionMessageHandler(command); if (!handler) { - return; + return null; } const messageResponse = handler({ message, sender }); - if (!messageResponse) { - return; + if (typeof messageResponse === "undefined") { + return null; } - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - Promise.resolve(messageResponse).then((response) => sendResponse(response)); + void Promise.resolve(messageResponse).then((response) => sendResponse(response)); return true; }; + /** + * Gets the extension message handler for the given command. + * + * @param command - The extension message command. + */ + private getExtensionMessageHandler(command: string): CallableFunction | undefined { + if (this.autofillOverlayContentService?.messageHandlers?.[command]) { + return this.autofillOverlayContentService.messageHandlers[command]; + } + + if (this.autofillInlineMenuContentService?.messageHandlers?.[command]) { + return this.autofillInlineMenuContentService.messageHandlers[command]; + } + + if (this.overlayNotificationsContentService?.messageHandlers?.[command]) { + return this.overlayNotificationsContentService.messageHandlers[command]; + } + + return this.extensionMessageHandlers[command]; + } + /** * Handles destroying the autofill init content script. Removes all * listeners, timeouts, and object instances to prevent memory leaks. @@ -304,6 +223,8 @@ class AutofillInit implements AutofillInitInterface { chrome.runtime.onMessage.removeListener(this.handleExtensionMessage); this.collectAutofillContentService.destroy(); this.autofillOverlayContentService?.destroy(); + this.autofillInlineMenuContentService?.destroy(); + this.overlayNotificationsContentService?.destroy(); } } diff --git a/apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts b/apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts new file mode 100644 index 00000000000..aed0f6cb940 --- /dev/null +++ b/apps/browser/src/autofill/content/bootstrap-autofill-overlay-menu.ts @@ -0,0 +1,30 @@ +import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service"; +import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; +import { DomQueryService } from "../services/dom-query.service"; +import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service"; +import { setupAutofillInitDisconnectAction } from "../utils"; + +import AutofillInit from "./autofill-init"; + +(function (windowContext) { + if (!windowContext.bitwardenAutofillInit) { + const domQueryService = new DomQueryService(); + const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); + const autofillOverlayContentService = new AutofillOverlayContentService( + domQueryService, + inlineMenuFieldQualificationService, + ); + let inlineMenuElements: AutofillInlineMenuContentService; + if (globalThis.self === globalThis.top) { + inlineMenuElements = new AutofillInlineMenuContentService(); + } + windowContext.bitwardenAutofillInit = new AutofillInit( + domQueryService, + autofillOverlayContentService, + inlineMenuElements, + ); + setupAutofillInitDisconnectAction(windowContext); + + windowContext.bitwardenAutofillInit.init(); + } +})(window); diff --git a/apps/browser/src/autofill/content/bootstrap-autofill-overlay-notifications.ts b/apps/browser/src/autofill/content/bootstrap-autofill-overlay-notifications.ts new file mode 100644 index 00000000000..0a810c68f56 --- /dev/null +++ b/apps/browser/src/autofill/content/bootstrap-autofill-overlay-notifications.ts @@ -0,0 +1,33 @@ +import { OverlayNotificationsContentService } from "../overlay/notifications/content/overlay-notifications-content.service"; +import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; +import { DomQueryService } from "../services/dom-query.service"; +import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service"; +import { setupAutofillInitDisconnectAction } from "../utils"; + +import AutofillInit from "./autofill-init"; + +(function (windowContext) { + if (!windowContext.bitwardenAutofillInit) { + const domQueryService = new DomQueryService(); + const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); + const autofillOverlayContentService = new AutofillOverlayContentService( + domQueryService, + inlineMenuFieldQualificationService, + ); + + let overlayNotificationsContentService: OverlayNotificationsContentService; + if (globalThis.self === globalThis.top) { + overlayNotificationsContentService = new OverlayNotificationsContentService(); + } + + windowContext.bitwardenAutofillInit = new AutofillInit( + domQueryService, + autofillOverlayContentService, + null, + overlayNotificationsContentService, + ); + setupAutofillInitDisconnectAction(windowContext); + + windowContext.bitwardenAutofillInit.init(); + } +})(window); diff --git a/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts b/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts index ab21e367c29..6df9397f6d8 100644 --- a/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts +++ b/apps/browser/src/autofill/content/bootstrap-autofill-overlay.ts @@ -1,12 +1,34 @@ -import AutofillOverlayContentService from "../services/autofill-overlay-content.service"; +import { AutofillInlineMenuContentService } from "../overlay/inline-menu/content/autofill-inline-menu-content.service"; +import { OverlayNotificationsContentService } from "../overlay/notifications/content/overlay-notifications-content.service"; +import { AutofillOverlayContentService } from "../services/autofill-overlay-content.service"; +import { DomQueryService } from "../services/dom-query.service"; +import { InlineMenuFieldQualificationService } from "../services/inline-menu-field-qualification.service"; import { setupAutofillInitDisconnectAction } from "../utils"; import AutofillInit from "./autofill-init"; (function (windowContext) { if (!windowContext.bitwardenAutofillInit) { - const autofillOverlayContentService = new AutofillOverlayContentService(); - windowContext.bitwardenAutofillInit = new AutofillInit(autofillOverlayContentService); + const domQueryService = new DomQueryService(); + const inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); + const autofillOverlayContentService = new AutofillOverlayContentService( + domQueryService, + inlineMenuFieldQualificationService, + ); + + let inlineMenuElements: AutofillInlineMenuContentService; + let overlayNotificationsContentService: OverlayNotificationsContentService; + if (globalThis.self === globalThis.top) { + inlineMenuElements = new AutofillInlineMenuContentService(); + overlayNotificationsContentService = new OverlayNotificationsContentService(); + } + + windowContext.bitwardenAutofillInit = new AutofillInit( + domQueryService, + autofillOverlayContentService, + inlineMenuElements, + overlayNotificationsContentService, + ); setupAutofillInitDisconnectAction(windowContext); windowContext.bitwardenAutofillInit.init(); diff --git a/apps/browser/src/autofill/content/bootstrap-autofill.ts b/apps/browser/src/autofill/content/bootstrap-autofill.ts index f98d4bc1d72..3de750cd671 100644 --- a/apps/browser/src/autofill/content/bootstrap-autofill.ts +++ b/apps/browser/src/autofill/content/bootstrap-autofill.ts @@ -1,10 +1,12 @@ +import { DomQueryService } from "../services/dom-query.service"; import { setupAutofillInitDisconnectAction } from "../utils"; import AutofillInit from "./autofill-init"; (function (windowContext) { if (!windowContext.bitwardenAutofillInit) { - windowContext.bitwardenAutofillInit = new AutofillInit(); + const domQueryService = new DomQueryService(); + windowContext.bitwardenAutofillInit = new AutofillInit(domQueryService); setupAutofillInitDisconnectAction(windowContext); windowContext.bitwardenAutofillInit.init(); diff --git a/apps/browser/src/autofill/content/notification-bar.ts b/apps/browser/src/autofill/content/notification-bar.ts index 2bcf4394fd9..5217ebbe8ed 100644 --- a/apps/browser/src/autofill/content/notification-bar.ts +++ b/apps/browser/src/autofill/content/notification-bar.ts @@ -6,6 +6,7 @@ import { import AutofillField from "../models/autofill-field"; import { WatchedForm } from "../models/watched-form"; import { NotificationBarIframeInitData } from "../notification/abstractions/notification-bar"; +import { NotificationTypeData } from "../overlay/notifications/abstractions/overlay-notifications-content.service"; import { FormData } from "../services/abstractions/autofill.service"; import { sendExtensionMessage, setupExtensionDisconnectAction } from "../utils"; @@ -832,7 +833,7 @@ async function loadNotificationBar() { // End Form Detection and Submission Handling // Notification Bar Functions (open, close, height adjustment, etc.) - function closeExistingAndOpenBar(type: string, typeData: any) { + function closeExistingAndOpenBar(type: string, typeData: NotificationTypeData) { const notificationBarInitData: NotificationBarIframeInitData = { type, isVaultLocked: typeData.isVaultLocked, diff --git a/apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts b/apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts new file mode 100644 index 00000000000..88b78dc2495 --- /dev/null +++ b/apps/browser/src/autofill/deprecated/background/abstractions/overlay.background.deprecated.ts @@ -0,0 +1,124 @@ +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; + +import { LockedVaultPendingNotificationsData } from "../../../background/abstractions/notification.background"; +import AutofillPageDetails from "../../../models/autofill-page-details"; + +type WebsiteIconData = { + imageEnabled: boolean; + image: string; + fallbackImage: string; + icon: string; +}; + +type OverlayAddNewItemMessage = { + login?: { + uri?: string; + hostname: string; + username: string; + password: string; + }; +}; + +type OverlayBackgroundExtensionMessage = { + [key: string]: any; + command: string; + tab?: chrome.tabs.Tab; + sender?: string; + details?: AutofillPageDetails; + overlayElement?: string; + display?: string; + data?: LockedVaultPendingNotificationsData; +} & OverlayAddNewItemMessage; + +type OverlayPortMessage = { + [key: string]: any; + command: string; + direction?: string; + overlayCipherId?: string; +}; + +type FocusedFieldData = { + focusedFieldStyles: Partial; + focusedFieldRects: Partial; + tabId?: number; +}; + +type OverlayCipherData = { + id: string; + name: string; + type: CipherType; + reprompt: CipherRepromptType; + favorite: boolean; + icon: { imageEnabled: boolean; image: string; fallbackImage: string; icon: string }; + login?: { username: string }; + card?: string; +}; + +type BackgroundMessageParam = { + message: OverlayBackgroundExtensionMessage; +}; +type BackgroundSenderParam = { + sender: chrome.runtime.MessageSender; +}; +type BackgroundOnMessageHandlerParams = BackgroundMessageParam & BackgroundSenderParam; + +type OverlayBackgroundExtensionMessageHandlers = { + [key: string]: CallableFunction; + openAutofillOverlay: () => void; + autofillOverlayElementClosed: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + autofillOverlayAddNewVaultItem: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + getAutofillOverlayVisibility: () => void; + checkAutofillOverlayFocused: () => void; + focusAutofillOverlayList: () => void; + updateAutofillOverlayPosition: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + updateAutofillOverlayHidden: ({ message }: BackgroundMessageParam) => void; + updateFocusedFieldData: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + collectPageDetailsResponse: ({ message, sender }: BackgroundOnMessageHandlerParams) => void; + unlockCompleted: ({ message }: BackgroundMessageParam) => void; + addedCipher: () => void; + addEditCipherSubmitted: () => void; + editedCipher: () => void; + deletedCipher: () => void; +}; + +type PortMessageParam = { + message: OverlayPortMessage; +}; +type PortConnectionParam = { + port: chrome.runtime.Port; +}; +type PortOnMessageHandlerParams = PortMessageParam & PortConnectionParam; + +type OverlayButtonPortMessageHandlers = { + [key: string]: CallableFunction; + overlayButtonClicked: ({ port }: PortConnectionParam) => void; + closeAutofillOverlay: ({ port }: PortConnectionParam) => void; + forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void; + overlayPageBlurred: () => void; + redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; +}; + +type OverlayListPortMessageHandlers = { + [key: string]: CallableFunction; + checkAutofillOverlayButtonFocused: () => void; + forceCloseAutofillOverlay: ({ port }: PortConnectionParam) => void; + overlayPageBlurred: () => void; + unlockVault: ({ port }: PortConnectionParam) => void; + fillSelectedListItem: ({ message, port }: PortOnMessageHandlerParams) => void; + addNewVaultItem: ({ port }: PortConnectionParam) => void; + viewSelectedCipher: ({ message, port }: PortOnMessageHandlerParams) => void; + redirectOverlayFocusOut: ({ message, port }: PortOnMessageHandlerParams) => void; +}; + +export { + WebsiteIconData, + OverlayBackgroundExtensionMessage, + OverlayPortMessage, + FocusedFieldData, + OverlayCipherData, + OverlayAddNewItemMessage, + OverlayBackgroundExtensionMessageHandlers, + OverlayButtonPortMessageHandlers, + OverlayListPortMessageHandlers, +}; diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts new file mode 100644 index 00000000000..3adaf9e276c --- /dev/null +++ b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts @@ -0,0 +1,1463 @@ +import { mock, MockProxy, mockReset } from "jest-mock-extended"; +import { BehaviorSubject, of } from "rxjs"; + +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { AuthService } from "@bitwarden/common/auth/services/auth.service"; +import { + SHOW_AUTOFILL_BUTTON, + AutofillOverlayVisibility, +} from "@bitwarden/common/autofill/constants"; +import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { + DefaultDomainSettingsService, + DomainSettingsService, +} from "@bitwarden/common/autofill/services/domain-settings.service"; +import { + EnvironmentService, + Region, +} from "@bitwarden/common/platform/abstractions/environment.service"; +import { ThemeType } from "@bitwarden/common/platform/enums"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { CloudEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; +import { I18nService } from "@bitwarden/common/platform/services/i18n.service"; +import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { + FakeStateProvider, + FakeAccountService, + mockAccountServiceWith, +} from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; +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 { CipherService } from "@bitwarden/common/vault/services/cipher.service"; + +import { BrowserApi } from "../../../platform/browser/browser-api"; +import { BrowserPlatformUtilsService } from "../../../platform/services/platform-utils/browser-platform-utils.service"; +import { + AutofillOverlayElement, + AutofillOverlayPort, + RedirectFocusDirection, +} from "../../enums/autofill-overlay.enum"; +import { AutofillService } from "../../services/abstractions/autofill.service"; +import { + createAutofillPageDetailsMock, + createChromeTabMock, + createFocusedFieldDataMock, + createPageDetailMock, + createPortSpyMock, +} from "../../spec/autofill-mocks"; +import { flushPromises, sendMockExtensionMessage, sendPortMessage } from "../../spec/testing-utils"; + +import LegacyOverlayBackground from "./overlay.background.deprecated"; + +describe("OverlayBackground", () => { + const mockUserId = Utils.newGuid() as UserId; + const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); + const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService); + let domainSettingsService: DomainSettingsService; + let buttonPortSpy: chrome.runtime.Port; + let listPortSpy: chrome.runtime.Port; + let overlayBackground: LegacyOverlayBackground; + const cipherService = mock(); + const autofillService = mock(); + let activeAccountStatusMock$: BehaviorSubject; + let authService: MockProxy; + + const environmentService = mock(); + environmentService.environment$ = new BehaviorSubject( + new CloudEnvironment({ + key: Region.US, + domain: "bitwarden.com", + urls: { icons: "https://icons.bitwarden.com/" }, + }), + ); + const autofillSettingsService = mock(); + const i18nService = mock(); + const platformUtilsService = mock(); + const themeStateService = mock(); + const initOverlayElementPorts = async (options = { initList: true, initButton: true }) => { + const { initList, initButton } = options; + if (initButton) { + await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.Button)); + buttonPortSpy = overlayBackground["overlayButtonPort"]; + } + + if (initList) { + await overlayBackground["handlePortOnConnect"](createPortSpyMock(AutofillOverlayPort.List)); + listPortSpy = overlayBackground["overlayListPort"]; + } + + return { buttonPortSpy, listPortSpy }; + }; + + beforeEach(() => { + domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); + activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked); + authService = mock(); + authService.activeAccountStatus$ = activeAccountStatusMock$; + overlayBackground = new LegacyOverlayBackground( + cipherService, + autofillService, + authService, + environmentService, + domainSettingsService, + autofillSettingsService, + i18nService, + platformUtilsService, + themeStateService, + ); + + jest + .spyOn(overlayBackground as any, "getOverlayVisibility") + .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); + + themeStateService.selectedTheme$ = of(ThemeType.Light); + domainSettingsService.showFavicons$ = of(true); + + void overlayBackground.init(); + }); + + afterEach(() => { + jest.clearAllMocks(); + mockReset(cipherService); + }); + + describe("removePageDetails", () => { + it("removes the page details for a specific tab from the pageDetailsForTab object", () => { + const tabId = 1; + const frameId = 2; + overlayBackground["pageDetailsForTab"][tabId] = new Map([[frameId, createPageDetailMock()]]); + overlayBackground.removePageDetails(tabId); + + expect(overlayBackground["pageDetailsForTab"][tabId]).toBeUndefined(); + }); + }); + + describe("init", () => { + it("sets up the extension message listeners, get the overlay's visibility settings, and get the user's auth status", async () => { + overlayBackground["setupExtensionMessageListeners"] = jest.fn(); + overlayBackground["getOverlayVisibility"] = jest.fn(); + overlayBackground["getAuthStatus"] = jest.fn(); + + await overlayBackground.init(); + + expect(overlayBackground["setupExtensionMessageListeners"]).toHaveBeenCalled(); + expect(overlayBackground["getOverlayVisibility"]).toHaveBeenCalled(); + expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); + }); + }); + + describe("updateOverlayCiphers", () => { + const url = "https://jest-testing-website.com"; + const tab = createChromeTabMock({ url }); + const cipher1 = mock({ + id: "id-1", + localData: { lastUsedDate: 222 }, + name: "name-1", + type: CipherType.Login, + login: { username: "username-1", uri: url }, + }); + const cipher2 = mock({ + id: "id-2", + localData: { lastUsedDate: 111 }, + name: "name-2", + type: CipherType.Login, + login: { username: "username-2", uri: url }, + }); + + beforeEach(() => { + activeAccountStatusMock$.next(AuthenticationStatus.Unlocked); + }); + + it("ignores updating the overlay ciphers if the user's auth status is not unlocked", async () => { + activeAccountStatusMock$.next(AuthenticationStatus.Locked); + jest.spyOn(BrowserApi, "getTabFromCurrentWindowId"); + jest.spyOn(cipherService, "getAllDecryptedForUrl"); + + await overlayBackground.updateOverlayCiphers(); + + expect(BrowserApi.getTabFromCurrentWindowId).not.toHaveBeenCalled(); + expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled(); + }); + + it("ignores updating the overlay ciphers if the tab is undefined", async () => { + jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(undefined); + jest.spyOn(cipherService, "getAllDecryptedForUrl"); + + await overlayBackground.updateOverlayCiphers(); + + expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); + expect(cipherService.getAllDecryptedForUrl).not.toHaveBeenCalled(); + }); + + it("queries all ciphers for the given url, sort them by last used, and format them for usage in the overlay", async () => { + jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(tab); + cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); + cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); + jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); + jest.spyOn(overlayBackground as any, "getOverlayCipherData"); + + await overlayBackground.updateOverlayCiphers(); + + expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); + expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url); + expect(overlayBackground["cipherService"].sortCiphersByLastUsedThenName).toHaveBeenCalled(); + expect(overlayBackground["overlayLoginCiphers"]).toStrictEqual( + new Map([ + ["overlay-cipher-0", cipher2], + ["overlay-cipher-1", cipher1], + ]), + ); + expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled(); + }); + + it("posts an `updateOverlayListCiphers` message to the overlay list port, and send a `updateIsOverlayCiphersPopulated` message to the tab indicating that the list of ciphers is populated", async () => { + overlayBackground["overlayListPort"] = mock(); + cipherService.getAllDecryptedForUrl.mockResolvedValue([cipher1, cipher2]); + cipherService.sortCiphersByLastUsedThenName.mockReturnValue(-1); + jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(tab); + jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); + + await overlayBackground.updateOverlayCiphers(); + + expect(overlayBackground["overlayListPort"].postMessage).toHaveBeenCalledWith({ + command: "updateOverlayListCiphers", + ciphers: [ + { + card: null, + favorite: cipher2.favorite, + icon: { + fallbackImage: "images/bwi-globe.png", + icon: "bwi-globe", + image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", + imageEnabled: true, + }, + id: "overlay-cipher-0", + login: { + username: "username-2", + }, + name: "name-2", + reprompt: cipher2.reprompt, + type: 1, + }, + { + card: null, + favorite: cipher1.favorite, + icon: { + fallbackImage: "images/bwi-globe.png", + icon: "bwi-globe", + image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", + imageEnabled: true, + }, + id: "overlay-cipher-1", + login: { + username: "username-1", + }, + name: "name-1", + reprompt: cipher1.reprompt, + type: 1, + }, + ], + }); + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + tab, + "updateIsOverlayCiphersPopulated", + { isOverlayCiphersPopulated: true }, + ); + }); + }); + + describe("getOverlayCipherData", () => { + const url = "https://jest-testing-website.com"; + const cipher1 = mock({ + id: "id-1", + localData: { lastUsedDate: 222 }, + name: "name-1", + type: CipherType.Login, + login: { username: "username-1", uri: url }, + }); + const cipher2 = mock({ + id: "id-2", + localData: { lastUsedDate: 111 }, + name: "name-2", + type: CipherType.Login, + login: { username: "username-2", uri: url }, + }); + const cipher3 = mock({ + id: "id-3", + localData: { lastUsedDate: 333 }, + name: "name-3", + type: CipherType.Card, + card: { subTitle: "Visa, *6789" }, + }); + const cipher4 = mock({ + id: "id-4", + localData: { lastUsedDate: 444 }, + name: "name-4", + type: CipherType.Card, + card: { subTitle: "Mastercard, *1234" }, + }); + + it("formats and returns the cipher data", async () => { + overlayBackground["overlayLoginCiphers"] = new Map([ + ["overlay-cipher-0", cipher2], + ["overlay-cipher-1", cipher1], + ["overlay-cipher-2", cipher3], + ["overlay-cipher-3", cipher4], + ]); + + const overlayCipherData = await overlayBackground["getOverlayCipherData"](); + + expect(overlayCipherData).toStrictEqual([ + { + card: null, + favorite: cipher2.favorite, + icon: { + fallbackImage: "images/bwi-globe.png", + icon: "bwi-globe", + image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", + imageEnabled: true, + }, + id: "overlay-cipher-0", + login: { + username: "username-2", + }, + name: "name-2", + reprompt: cipher2.reprompt, + type: 1, + }, + { + card: null, + favorite: cipher1.favorite, + icon: { + fallbackImage: "images/bwi-globe.png", + icon: "bwi-globe", + image: "https://icons.bitwarden.com//jest-testing-website.com/icon.png", + imageEnabled: true, + }, + id: "overlay-cipher-1", + login: { + username: "username-1", + }, + name: "name-1", + reprompt: cipher1.reprompt, + type: 1, + }, + { + card: "Visa, *6789", + favorite: cipher3.favorite, + icon: { + fallbackImage: "", + icon: "bwi-credit-card", + image: undefined, + imageEnabled: true, + }, + id: "overlay-cipher-2", + login: null, + name: "name-3", + reprompt: cipher3.reprompt, + type: 3, + }, + { + card: "Mastercard, *1234", + favorite: cipher4.favorite, + icon: { + fallbackImage: "", + icon: "bwi-credit-card", + image: undefined, + imageEnabled: true, + }, + id: "overlay-cipher-3", + login: null, + name: "name-4", + reprompt: cipher4.reprompt, + type: 3, + }, + ]); + }); + }); + + describe("getAuthStatus", () => { + it("will update the user's auth status but will not update the overlay ciphers", async () => { + const authStatus = AuthenticationStatus.Unlocked; + overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; + jest.spyOn(overlayBackground["authService"], "getAuthStatus").mockResolvedValue(authStatus); + jest.spyOn(overlayBackground as any, "updateOverlayButtonAuthStatus").mockImplementation(); + jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation(); + + const status = await overlayBackground["getAuthStatus"](); + + expect(overlayBackground["authService"].getAuthStatus).toHaveBeenCalled(); + expect(overlayBackground["updateOverlayButtonAuthStatus"]).not.toHaveBeenCalled(); + expect(overlayBackground["updateOverlayCiphers"]).not.toHaveBeenCalled(); + expect(overlayBackground["userAuthStatus"]).toBe(authStatus); + expect(status).toBe(authStatus); + }); + + it("will update the user's auth status and update the overlay ciphers if the status has been modified", async () => { + const authStatus = AuthenticationStatus.Unlocked; + overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; + jest.spyOn(overlayBackground["authService"], "getAuthStatus").mockResolvedValue(authStatus); + jest.spyOn(overlayBackground as any, "updateOverlayButtonAuthStatus").mockImplementation(); + jest.spyOn(overlayBackground as any, "updateOverlayCiphers").mockImplementation(); + + await overlayBackground["getAuthStatus"](); + + expect(overlayBackground["authService"].getAuthStatus).toHaveBeenCalled(); + expect(overlayBackground["updateOverlayButtonAuthStatus"]).toHaveBeenCalled(); + expect(overlayBackground["updateOverlayCiphers"]).toHaveBeenCalled(); + expect(overlayBackground["userAuthStatus"]).toBe(authStatus); + }); + }); + + describe("updateOverlayButtonAuthStatus", () => { + it("will send a message to the button port with the user's auth status", () => { + overlayBackground["overlayButtonPort"] = mock(); + jest.spyOn(overlayBackground["overlayButtonPort"], "postMessage"); + + overlayBackground["updateOverlayButtonAuthStatus"](); + + expect(overlayBackground["overlayButtonPort"].postMessage).toHaveBeenCalledWith({ + command: "updateOverlayButtonAuthStatus", + authStatus: overlayBackground["userAuthStatus"], + }); + }); + }); + + describe("getTranslations", () => { + it("will query the overlay page translations if they have not been queried", () => { + overlayBackground["overlayPageTranslations"] = undefined; + jest.spyOn(overlayBackground as any, "getTranslations"); + jest.spyOn(overlayBackground["i18nService"], "translate").mockImplementation((key) => key); + jest.spyOn(BrowserApi, "getUILanguage").mockReturnValue("en"); + + const translations = overlayBackground["getTranslations"](); + + expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); + const translationKeys = [ + "opensInANewWindow", + "bitwardenOverlayButton", + "toggleBitwardenVaultOverlay", + "bitwardenVault", + "unlockYourAccountToViewMatchingLogins", + "unlockAccount", + "fillCredentialsFor", + "partialUsername", + "view", + "noItemsToShow", + "newItem", + "addNewVaultItem", + ]; + translationKeys.forEach((key) => { + expect(overlayBackground["i18nService"].translate).toHaveBeenCalledWith(key); + }); + expect(translations).toStrictEqual({ + locale: "en", + opensInANewWindow: "opensInANewWindow", + buttonPageTitle: "bitwardenOverlayButton", + toggleBitwardenVaultOverlay: "toggleBitwardenVaultOverlay", + listPageTitle: "bitwardenVault", + unlockYourAccount: "unlockYourAccountToViewMatchingLogins", + unlockAccount: "unlockAccount", + fillCredentialsFor: "fillCredentialsFor", + partialUsername: "partialUsername", + view: "view", + noItemsToShow: "noItemsToShow", + newItem: "newItem", + addNewVaultItem: "addNewVaultItem", + }); + }); + }); + + describe("setupExtensionMessageListeners", () => { + it("will set up onMessage and onConnect listeners", () => { + overlayBackground["setupExtensionMessageListeners"](); + + // eslint-disable-next-line + expect(chrome.runtime.onMessage.addListener).toHaveBeenCalled(); + expect(chrome.runtime.onConnect.addListener).toHaveBeenCalled(); + }); + }); + + describe("handleExtensionMessage", () => { + it("will return early if the message command is not present within the extensionMessageHandlers", () => { + const message = { + command: "not-a-command", + }; + const sender = mock({ tab: { id: 1 } }); + const sendResponse = jest.fn(); + + const returnValue = overlayBackground["handleExtensionMessage"]( + message, + sender, + sendResponse, + ); + + expect(returnValue).toBe(null); + expect(sendResponse).not.toHaveBeenCalled(); + }); + + it("will trigger the message handler and return undefined if the message does not have a response", () => { + const message = { + command: "autofillOverlayElementClosed", + }; + const sender = mock({ tab: { id: 1 } }); + const sendResponse = jest.fn(); + jest.spyOn(overlayBackground as any, "overlayElementClosed"); + + const returnValue = overlayBackground["handleExtensionMessage"]( + message, + sender, + sendResponse, + ); + + expect(returnValue).toBe(null); + expect(sendResponse).not.toHaveBeenCalled(); + expect(overlayBackground["overlayElementClosed"]).toHaveBeenCalledWith(message, sender); + }); + + it("will return a response if the message handler returns a response", async () => { + const message = { + command: "openAutofillOverlay", + }; + const sender = mock({ tab: { id: 1 } }); + const sendResponse = jest.fn(); + jest.spyOn(overlayBackground as any, "getTranslations").mockReturnValue("translations"); + + const returnValue = overlayBackground["handleExtensionMessage"]( + message, + sender, + sendResponse, + ); + + expect(returnValue).toBe(true); + }); + + describe("extension message handlers", () => { + beforeEach(() => { + jest + .spyOn(overlayBackground as any, "getAuthStatus") + .mockResolvedValue(AuthenticationStatus.Unlocked); + }); + + describe("openAutofillOverlay message handler", () => { + it("opens the autofill overlay by sending a message to the current tab", async () => { + const sender = mock({ tab: { id: 1 } }); + jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(sender.tab); + jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); + + sendMockExtensionMessage({ command: "openAutofillOverlay" }); + await flushPromises(); + + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + sender.tab, + "openAutofillOverlay", + { + isFocusingFieldElement: false, + isOpeningFullOverlay: false, + authStatus: AuthenticationStatus.Unlocked, + }, + ); + }); + }); + + describe("autofillOverlayElementClosed message handler", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + }); + + it("disconnects any expired ports if the sender is not from the same page as the most recently focused field", () => { + const port1 = mock(); + const port2 = mock(); + overlayBackground["expiredPorts"] = [port1, port2]; + const sender = mock({ tab: { id: 1 } }); + const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage( + { + command: "autofillOverlayElementClosed", + overlayElement: AutofillOverlayElement.Button, + }, + sender, + ); + + expect(port1.disconnect).toHaveBeenCalled(); + expect(port2.disconnect).toHaveBeenCalled(); + }); + + it("disconnects the button element port", () => { + sendMockExtensionMessage({ + command: "autofillOverlayElementClosed", + overlayElement: AutofillOverlayElement.Button, + }); + + expect(buttonPortSpy.disconnect).toHaveBeenCalled(); + expect(overlayBackground["overlayButtonPort"]).toBeNull(); + }); + + it("disconnects the list element port", () => { + sendMockExtensionMessage({ + command: "autofillOverlayElementClosed", + overlayElement: AutofillOverlayElement.List, + }); + + expect(listPortSpy.disconnect).toHaveBeenCalled(); + expect(overlayBackground["overlayListPort"]).toBeNull(); + }); + }); + + describe("autofillOverlayAddNewVaultItem message handler", () => { + let sender: chrome.runtime.MessageSender; + beforeEach(() => { + sender = mock({ tab: { id: 1 } }); + jest + .spyOn(overlayBackground["cipherService"], "setAddEditCipherInfo") + .mockImplementation(); + jest.spyOn(overlayBackground as any, "openAddEditVaultItemPopout").mockImplementation(); + }); + + it("will not open the add edit popout window if the message does not have a login cipher provided", () => { + sendMockExtensionMessage({ command: "autofillOverlayAddNewVaultItem" }, sender); + + expect(overlayBackground["cipherService"].setAddEditCipherInfo).not.toHaveBeenCalled(); + expect(overlayBackground["openAddEditVaultItemPopout"]).not.toHaveBeenCalled(); + }); + + it("will open the add edit popout window after creating a new cipher", async () => { + jest.spyOn(BrowserApi, "sendMessage"); + + sendMockExtensionMessage( + { + command: "autofillOverlayAddNewVaultItem", + login: { + uri: "https://tacos.com", + hostname: "", + username: "username", + password: "password", + }, + }, + sender, + ); + await flushPromises(); + + expect(overlayBackground["cipherService"].setAddEditCipherInfo).toHaveBeenCalled(); + expect(BrowserApi.sendMessage).toHaveBeenCalledWith( + "inlineAutofillMenuRefreshAddEditCipher", + ); + expect(overlayBackground["openAddEditVaultItemPopout"]).toHaveBeenCalled(); + }); + }); + + describe("getAutofillOverlayVisibility message handler", () => { + beforeEach(() => { + jest + .spyOn(overlayBackground as any, "getOverlayVisibility") + .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); + }); + + it("will set the overlayVisibility property", async () => { + sendMockExtensionMessage({ command: "getAutofillOverlayVisibility" }); + await flushPromises(); + + expect(await overlayBackground["getOverlayVisibility"]()).toBe( + AutofillOverlayVisibility.OnFieldFocus, + ); + }); + + it("returns the overlayVisibility property", async () => { + const sendMessageSpy = jest.fn(); + + sendMockExtensionMessage( + { command: "getAutofillOverlayVisibility" }, + undefined, + sendMessageSpy, + ); + await flushPromises(); + + expect(sendMessageSpy).toHaveBeenCalledWith(AutofillOverlayVisibility.OnFieldFocus); + }); + }); + + describe("checkAutofillOverlayFocused message handler", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + }); + + it("will check if the overlay list is focused if the list port is open", () => { + sendMockExtensionMessage({ command: "checkAutofillOverlayFocused" }); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillOverlayListFocused", + }); + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillOverlayButtonFocused", + }); + }); + + it("will check if the overlay button is focused if the list port is not open", () => { + overlayBackground["overlayListPort"] = undefined; + + sendMockExtensionMessage({ command: "checkAutofillOverlayFocused" }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "checkAutofillOverlayButtonFocused", + }); + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "checkAutofillOverlayListFocused", + }); + }); + }); + + describe("focusAutofillOverlayList message handler", () => { + it("will send a `focusOverlayList` message to the overlay list port", async () => { + await initOverlayElementPorts({ initList: true, initButton: false }); + + sendMockExtensionMessage({ command: "focusAutofillOverlayList" }); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ command: "focusOverlayList" }); + }); + }); + + describe("updateAutofillOverlayPosition message handler", () => { + beforeEach(async () => { + await overlayBackground["handlePortOnConnect"]( + createPortSpyMock(AutofillOverlayPort.List), + ); + listPortSpy = overlayBackground["overlayListPort"]; + + await overlayBackground["handlePortOnConnect"]( + createPortSpyMock(AutofillOverlayPort.Button), + ); + buttonPortSpy = overlayBackground["overlayButtonPort"]; + }); + + it("ignores updating the position if the overlay element type is not provided", () => { + sendMockExtensionMessage({ command: "updateAutofillOverlayPosition" }); + + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + }); + + it("skips updating the position if the most recently focused field is different than the message sender", () => { + const sender = mock({ tab: { id: 1 } }); + const focusedFieldData = createFocusedFieldDataMock({ tabId: 2 }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ command: "updateAutofillOverlayPosition" }, sender); + + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: expect.anything(), + }); + }); + + it("updates the overlay button's position", () => { + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillOverlayPosition", + overlayElement: AutofillOverlayElement.Button, + }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: { height: "2px", left: "4px", top: "2px", width: "2px" }, + }); + }); + + it("modifies the overlay button's height for medium sized input elements", () => { + const focusedFieldData = createFocusedFieldDataMock({ + focusedFieldRects: { top: 1, left: 2, height: 35, width: 4 }, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillOverlayPosition", + overlayElement: AutofillOverlayElement.Button, + }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: { height: "20px", left: "-22px", top: "8px", width: "20px" }, + }); + }); + + it("modifies the overlay button's height for large sized input elements", () => { + const focusedFieldData = createFocusedFieldDataMock({ + focusedFieldRects: { top: 1, left: 2, height: 50, width: 4 }, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillOverlayPosition", + overlayElement: AutofillOverlayElement.Button, + }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: { height: "27px", left: "-32px", top: "13px", width: "27px" }, + }); + }); + + it("takes into account the right padding of the focused field in positioning the button if the right padding of the field is larger than the left padding", () => { + const focusedFieldData = createFocusedFieldDataMock({ + focusedFieldStyles: { paddingRight: "20px", paddingLeft: "6px" }, + }); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + sendMockExtensionMessage({ + command: "updateAutofillOverlayPosition", + overlayElement: AutofillOverlayElement.Button, + }); + + expect(buttonPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: { height: "2px", left: "-18px", top: "2px", width: "2px" }, + }); + }); + + it("will post a message to the overlay list facilitating an update of the list's position", () => { + const sender = mock({ tab: { id: 1 } }); + const focusedFieldData = createFocusedFieldDataMock(); + sendMockExtensionMessage({ command: "updateFocusedFieldData", focusedFieldData }); + + overlayBackground["updateOverlayPosition"]( + { overlayElement: AutofillOverlayElement.List }, + sender, + ); + sendMockExtensionMessage({ + command: "updateAutofillOverlayPosition", + overlayElement: AutofillOverlayElement.List, + }); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith({ + command: "updateIframePosition", + styles: { left: "2px", top: "4px", width: "4px" }, + }); + }); + }); + + describe("updateOverlayHidden", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + }); + + it("returns early if the display value is not provided", () => { + const message = { + command: "updateAutofillOverlayHidden", + }; + + sendMockExtensionMessage(message); + + expect(buttonPortSpy.postMessage).not.toHaveBeenCalledWith(message); + expect(listPortSpy.postMessage).not.toHaveBeenCalledWith(message); + }); + + it("posts a message to the overlay button and list with the display value", () => { + const message = { command: "updateAutofillOverlayHidden", display: "none" }; + + sendMockExtensionMessage(message); + + expect(overlayBackground["overlayButtonPort"].postMessage).toHaveBeenCalledWith({ + command: "updateOverlayHidden", + styles: { + display: message.display, + }, + }); + expect(overlayBackground["overlayListPort"].postMessage).toHaveBeenCalledWith({ + command: "updateOverlayHidden", + styles: { + display: message.display, + }, + }); + }); + }); + + describe("collectPageDetailsResponse message handler", () => { + let sender: chrome.runtime.MessageSender; + const pageDetails1 = createAutofillPageDetailsMock({ + login: { username: "username1", password: "password1" }, + }); + const pageDetails2 = createAutofillPageDetailsMock({ + login: { username: "username2", password: "password2" }, + }); + + beforeEach(() => { + sender = mock({ tab: { id: 1 } }); + }); + + it("stores the page details provided by the message by the tab id of the sender", () => { + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: pageDetails1 }, + sender, + ); + + expect(overlayBackground["pageDetailsForTab"][sender.tab.id]).toStrictEqual( + new Map([ + [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], + ]), + ); + }); + + it("updates the page details for a tab that already has a set of page details stored ", () => { + const secondFrameSender = mock({ + tab: { id: 1 }, + frameId: 3, + }); + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], + ]); + + sendMockExtensionMessage( + { command: "collectPageDetailsResponse", details: pageDetails2 }, + secondFrameSender, + ); + + expect(overlayBackground["pageDetailsForTab"][sender.tab.id]).toStrictEqual( + new Map([ + [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails1 }], + [ + secondFrameSender.frameId, + { + frameId: secondFrameSender.frameId, + tab: secondFrameSender.tab, + details: pageDetails2, + }, + ], + ]), + ); + }); + }); + + describe("unlockCompleted message handler", () => { + let getAuthStatusSpy: jest.SpyInstance; + + beforeEach(() => { + overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; + jest.spyOn(BrowserApi, "tabSendMessageData"); + getAuthStatusSpy = jest + .spyOn(overlayBackground as any, "getAuthStatus") + .mockImplementation(() => { + overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; + return Promise.resolve(AuthenticationStatus.Unlocked); + }); + }); + + it("updates the user's auth status but does not open the overlay", async () => { + const message = { + command: "unlockCompleted", + data: { + commandToRetry: { message: { command: "" } }, + }, + }; + + sendMockExtensionMessage(message); + await flushPromises(); + + expect(getAuthStatusSpy).toHaveBeenCalled(); + expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled(); + }); + + it("updates user's auth status and opens the overlay if a follow up command is provided", async () => { + const sender = mock({ tab: { id: 1 } }); + const message = { + command: "unlockCompleted", + data: { + commandToRetry: { message: { command: "openAutofillOverlay" } }, + }, + }; + jest.spyOn(BrowserApi, "getTabFromCurrentWindowId").mockResolvedValueOnce(sender.tab); + + sendMockExtensionMessage(message); + await flushPromises(); + + expect(getAuthStatusSpy).toHaveBeenCalled(); + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + sender.tab, + "openAutofillOverlay", + { + isFocusingFieldElement: true, + isOpeningFullOverlay: false, + authStatus: AuthenticationStatus.Unlocked, + }, + ); + }); + }); + + describe("extension messages that trigger an update of the inline menu ciphers", () => { + const extensionMessages = [ + "addedCipher", + "addEditCipherSubmitted", + "editedCipher", + "deletedCipher", + ]; + + beforeEach(() => { + jest.spyOn(overlayBackground, "updateOverlayCiphers").mockImplementation(); + }); + + extensionMessages.forEach((message) => { + it(`triggers an update of the overlay ciphers when the ${message} message is received`, () => { + sendMockExtensionMessage({ command: message }); + expect(overlayBackground.updateOverlayCiphers).toHaveBeenCalled(); + }); + }); + }); + }); + }); + + describe("handlePortOnConnect", () => { + beforeEach(() => { + jest.spyOn(overlayBackground as any, "updateOverlayPosition").mockImplementation(); + jest.spyOn(overlayBackground as any, "getAuthStatus").mockImplementation(); + jest.spyOn(overlayBackground as any, "getTranslations").mockImplementation(); + jest.spyOn(overlayBackground as any, "getOverlayCipherData").mockImplementation(); + }); + + it("skips setting up the overlay port if the port connection is not for an overlay element", async () => { + const port = createPortSpyMock("not-an-overlay-element"); + + await overlayBackground["handlePortOnConnect"](port); + + expect(port.onMessage.addListener).not.toHaveBeenCalled(); + expect(port.postMessage).not.toHaveBeenCalled(); + }); + + it("sets up the overlay list port if the port connection is for the overlay list", async () => { + await initOverlayElementPorts({ initList: true, initButton: false }); + await flushPromises(); + + expect(overlayBackground["overlayButtonPort"]).toBeUndefined(); + expect(listPortSpy.onMessage.addListener).toHaveBeenCalled(); + expect(listPortSpy.postMessage).toHaveBeenCalled(); + expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); + expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/list.css"); + expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); + expect(overlayBackground["getOverlayCipherData"]).toHaveBeenCalled(); + expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith( + { overlayElement: AutofillOverlayElement.List }, + listPortSpy.sender, + ); + }); + + it("sets up the overlay button port if the port connection is for the overlay button", async () => { + await initOverlayElementPorts({ initList: false, initButton: true }); + await flushPromises(); + + expect(overlayBackground["overlayListPort"]).toBeUndefined(); + expect(buttonPortSpy.onMessage.addListener).toHaveBeenCalled(); + expect(buttonPortSpy.postMessage).toHaveBeenCalled(); + expect(overlayBackground["getAuthStatus"]).toHaveBeenCalled(); + expect(chrome.runtime.getURL).toHaveBeenCalledWith("overlay/button.css"); + expect(overlayBackground["getTranslations"]).toHaveBeenCalled(); + expect(overlayBackground["updateOverlayPosition"]).toHaveBeenCalledWith( + { overlayElement: AutofillOverlayElement.Button }, + buttonPortSpy.sender, + ); + }); + + it("stores an existing overlay port so that it can be disconnected at a later time", async () => { + overlayBackground["overlayButtonPort"] = mock(); + + await initOverlayElementPorts({ initList: false, initButton: true }); + await flushPromises(); + + expect(overlayBackground["expiredPorts"].length).toBe(1); + }); + + it("gets the system theme", async () => { + themeStateService.selectedTheme$ = of(ThemeType.System); + + await initOverlayElementPorts({ initList: true, initButton: false }); + await flushPromises(); + + expect(listPortSpy.postMessage).toHaveBeenCalledWith( + expect.objectContaining({ theme: ThemeType.System }), + ); + }); + }); + + describe("handleOverlayElementPortMessage", () => { + beforeEach(async () => { + await initOverlayElementPorts(); + overlayBackground["userAuthStatus"] = AuthenticationStatus.Unlocked; + }); + + it("ignores port messages that do not contain a handler", () => { + jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); + + sendPortMessage(buttonPortSpy, { command: "checkAutofillOverlayButtonFocused" }); + + expect(overlayBackground["checkOverlayButtonFocused"]).not.toHaveBeenCalled(); + }); + + describe("overlay button message handlers", () => { + it("unlocks the vault if the user auth status is not unlocked", () => { + overlayBackground["userAuthStatus"] = AuthenticationStatus.LoggedOut; + jest.spyOn(overlayBackground as any, "unlockVault").mockImplementation(); + + sendPortMessage(buttonPortSpy, { command: "overlayButtonClicked" }); + + expect(overlayBackground["unlockVault"]).toHaveBeenCalled(); + }); + + it("opens the autofill overlay if the auth status is unlocked", () => { + jest.spyOn(overlayBackground as any, "openOverlay").mockImplementation(); + + sendPortMessage(buttonPortSpy, { command: "overlayButtonClicked" }); + + expect(overlayBackground["openOverlay"]).toHaveBeenCalled(); + }); + + describe("closeAutofillOverlay", () => { + it("sends a `closeOverlay` message to the sender tab", () => { + jest.spyOn(BrowserApi, "tabSendMessageData"); + + sendPortMessage(buttonPortSpy, { command: "closeAutofillOverlay" }); + + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + buttonPortSpy.sender.tab, + "closeAutofillOverlay", + { forceCloseOverlay: false }, + ); + }); + }); + + describe("forceCloseAutofillOverlay", () => { + it("sends a `closeOverlay` message to the sender tab with a `forceCloseOverlay` flag of `true` set", () => { + jest.spyOn(BrowserApi, "tabSendMessageData"); + + sendPortMessage(buttonPortSpy, { command: "forceCloseAutofillOverlay" }); + + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + buttonPortSpy.sender.tab, + "closeAutofillOverlay", + { forceCloseOverlay: true }, + ); + }); + }); + + describe("overlayPageBlurred", () => { + it("checks if the overlay list is focused", () => { + jest.spyOn(overlayBackground as any, "checkOverlayListFocused"); + + sendPortMessage(buttonPortSpy, { command: "overlayPageBlurred" }); + + expect(overlayBackground["checkOverlayListFocused"]).toHaveBeenCalled(); + }); + }); + + describe("redirectOverlayFocusOut", () => { + beforeEach(() => { + jest.spyOn(BrowserApi, "tabSendMessageData"); + }); + + it("ignores the redirect message if the direction is not provided", () => { + sendPortMessage(buttonPortSpy, { command: "redirectOverlayFocusOut" }); + + expect(BrowserApi.tabSendMessageData).not.toHaveBeenCalled(); + }); + + it("sends the redirect message if the direction is provided", () => { + sendPortMessage(buttonPortSpy, { + command: "redirectOverlayFocusOut", + direction: RedirectFocusDirection.Next, + }); + + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + buttonPortSpy.sender.tab, + "redirectOverlayFocusOut", + { direction: RedirectFocusDirection.Next }, + ); + }); + }); + }); + + describe("overlay list message handlers", () => { + describe("checkAutofillOverlayButtonFocused", () => { + it("checks on the focus state of the overlay button", () => { + jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); + + sendPortMessage(listPortSpy, { command: "checkAutofillOverlayButtonFocused" }); + + expect(overlayBackground["checkOverlayButtonFocused"]).toHaveBeenCalled(); + }); + }); + + describe("forceCloseAutofillOverlay", () => { + it("sends a `closeOverlay` message to the sender tab with a `forceCloseOverlay` flag of `true` set", () => { + jest.spyOn(BrowserApi, "tabSendMessageData"); + + sendPortMessage(listPortSpy, { command: "forceCloseAutofillOverlay" }); + + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + listPortSpy.sender.tab, + "closeAutofillOverlay", + { forceCloseOverlay: true }, + ); + }); + }); + + describe("overlayPageBlurred", () => { + it("checks on the focus state of the overlay button", () => { + jest.spyOn(overlayBackground as any, "checkOverlayButtonFocused").mockImplementation(); + + sendPortMessage(listPortSpy, { command: "overlayPageBlurred" }); + + expect(overlayBackground["checkOverlayButtonFocused"]).toHaveBeenCalled(); + }); + }); + + describe("unlockVault", () => { + it("closes the autofill overlay and opens the unlock popout", async () => { + jest.spyOn(overlayBackground as any, "closeOverlay").mockImplementation(); + jest.spyOn(overlayBackground as any, "openUnlockPopout").mockImplementation(); + jest.spyOn(BrowserApi, "tabSendMessageData").mockImplementation(); + + sendPortMessage(listPortSpy, { command: "unlockVault" }); + await flushPromises(); + + expect(overlayBackground["closeOverlay"]).toHaveBeenCalledWith(listPortSpy); + expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( + listPortSpy.sender.tab, + "addToLockedVaultPendingNotifications", + { + commandToRetry: { + message: { command: "openAutofillOverlay" }, + sender: listPortSpy.sender, + }, + target: "overlay.background", + }, + ); + expect(overlayBackground["openUnlockPopout"]).toHaveBeenCalledWith( + listPortSpy.sender.tab, + true, + ); + }); + }); + + describe("fillSelectedListItem", () => { + let getLoginCiphersSpy: jest.SpyInstance; + let isPasswordRepromptRequiredSpy: jest.SpyInstance; + let doAutoFillSpy: jest.SpyInstance; + let sender: chrome.runtime.MessageSender; + const pageDetails = createAutofillPageDetailsMock({ + login: { username: "username1", password: "password1" }, + }); + + beforeEach(() => { + getLoginCiphersSpy = jest.spyOn(overlayBackground["overlayLoginCiphers"], "get"); + isPasswordRepromptRequiredSpy = jest.spyOn( + overlayBackground["autofillService"], + "isPasswordRepromptRequired", + ); + doAutoFillSpy = jest.spyOn(overlayBackground["autofillService"], "doAutoFill"); + sender = mock({ tab: { id: 1 } }); + }); + + it("ignores the fill request if the overlay cipher id is not provided", async () => { + sendPortMessage(listPortSpy, { command: "fillSelectedListItem" }); + await flushPromises(); + + expect(getLoginCiphersSpy).not.toHaveBeenCalled(); + expect(isPasswordRepromptRequiredSpy).not.toHaveBeenCalled(); + expect(doAutoFillSpy).not.toHaveBeenCalled(); + }); + + it("ignores the fill request if the tab does not contain any identified page details", async () => { + sendPortMessage(listPortSpy, { + command: "fillSelectedListItem", + overlayCipherId: "overlay-cipher-1", + }); + await flushPromises(); + + expect(getLoginCiphersSpy).not.toHaveBeenCalled(); + expect(isPasswordRepromptRequiredSpy).not.toHaveBeenCalled(); + expect(doAutoFillSpy).not.toHaveBeenCalled(); + }); + + it("ignores the fill request if a master password reprompt is required", async () => { + const cipher = mock({ + reprompt: CipherRepromptType.Password, + type: CipherType.Login, + }); + overlayBackground["overlayLoginCiphers"] = new Map([["overlay-cipher-1", cipher]]); + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], + ]); + getLoginCiphersSpy = jest.spyOn(overlayBackground["overlayLoginCiphers"], "get"); + isPasswordRepromptRequiredSpy.mockResolvedValue(true); + + sendPortMessage(listPortSpy, { + command: "fillSelectedListItem", + overlayCipherId: "overlay-cipher-1", + }); + await flushPromises(); + + expect(getLoginCiphersSpy).toHaveBeenCalled(); + expect(isPasswordRepromptRequiredSpy).toHaveBeenCalledWith( + cipher, + listPortSpy.sender.tab, + ); + expect(doAutoFillSpy).not.toHaveBeenCalled(); + }); + + it("autofills the selected cipher and move it to the top of the front of the ciphers map", async () => { + const cipher1 = mock({ id: "overlay-cipher-1" }); + const cipher2 = mock({ id: "overlay-cipher-2" }); + const cipher3 = mock({ id: "overlay-cipher-3" }); + overlayBackground["overlayLoginCiphers"] = new Map([ + ["overlay-cipher-1", cipher1], + ["overlay-cipher-2", cipher2], + ["overlay-cipher-3", cipher3], + ]); + const pageDetailsForTab = { + frameId: sender.frameId, + tab: sender.tab, + details: pageDetails, + }; + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, pageDetailsForTab], + ]); + isPasswordRepromptRequiredSpy.mockResolvedValue(false); + + sendPortMessage(listPortSpy, { + command: "fillSelectedListItem", + overlayCipherId: "overlay-cipher-2", + }); + await flushPromises(); + + expect(isPasswordRepromptRequiredSpy).toHaveBeenCalledWith( + cipher2, + listPortSpy.sender.tab, + ); + expect(doAutoFillSpy).toHaveBeenCalledWith({ + tab: listPortSpy.sender.tab, + cipher: cipher2, + pageDetails: [pageDetailsForTab], + fillNewPassword: true, + allowTotpAutofill: true, + }); + expect(overlayBackground["overlayLoginCiphers"].entries()).toStrictEqual( + new Map([ + ["overlay-cipher-2", cipher2], + ["overlay-cipher-1", cipher1], + ["overlay-cipher-3", cipher3], + ]).entries(), + ); + }); + + it("copies the cipher's totp code to the clipboard after filling", async () => { + const cipher1 = mock({ id: "overlay-cipher-1" }); + overlayBackground["overlayLoginCiphers"] = new Map([["overlay-cipher-1", cipher1]]); + overlayBackground["pageDetailsForTab"][sender.tab.id] = new Map([ + [sender.frameId, { frameId: sender.frameId, tab: sender.tab, details: pageDetails }], + ]); + isPasswordRepromptRequiredSpy.mockResolvedValue(false); + const copyToClipboardSpy = jest + .spyOn(overlayBackground["platformUtilsService"], "copyToClipboard") + .mockImplementation(); + doAutoFillSpy.mockReturnValueOnce("totp-code"); + + sendPortMessage(listPortSpy, { + command: "fillSelectedListItem", + overlayCipherId: "overlay-cipher-2", + }); + await flushPromises(); + + expect(copyToClipboardSpy).toHaveBeenCalledWith("totp-code"); + }); + }); + + describe("getNewVaultItemDetails", () => { + it("will send an addNewVaultItemFromOverlay message", async () => { + jest.spyOn(BrowserApi, "tabSendMessage"); + + sendPortMessage(listPortSpy, { command: "addNewVaultItem" }); + await flushPromises(); + + expect(BrowserApi.tabSendMessage).toHaveBeenCalledWith(listPortSpy.sender.tab, { + command: "addNewVaultItemFromOverlay", + }); + }); + }); + + describe("viewSelectedCipher", () => { + let openViewVaultItemPopoutSpy: jest.SpyInstance; + + beforeEach(() => { + openViewVaultItemPopoutSpy = jest + .spyOn(overlayBackground as any, "openViewVaultItemPopout") + .mockImplementation(); + }); + + it("returns early if the passed cipher ID does not match one of the overlay login ciphers", async () => { + overlayBackground["overlayLoginCiphers"] = new Map([ + ["overlay-cipher-0", mock({ id: "overlay-cipher-0" })], + ]); + + sendPortMessage(listPortSpy, { + command: "viewSelectedCipher", + overlayCipherId: "overlay-cipher-1", + }); + await flushPromises(); + + expect(openViewVaultItemPopoutSpy).not.toHaveBeenCalled(); + }); + + it("will open the view vault item popout with the selected cipher", async () => { + const cipher = mock({ id: "overlay-cipher-1" }); + overlayBackground["overlayLoginCiphers"] = new Map([ + ["overlay-cipher-0", mock({ id: "overlay-cipher-0" })], + ["overlay-cipher-1", cipher], + ]); + + sendPortMessage(listPortSpy, { + command: "viewSelectedCipher", + overlayCipherId: "overlay-cipher-1", + }); + await flushPromises(); + + expect(overlayBackground["openViewVaultItemPopout"]).toHaveBeenCalledWith( + listPortSpy.sender.tab, + { + cipherId: cipher.id, + action: SHOW_AUTOFILL_BUTTON, + }, + ); + }); + }); + + describe("redirectOverlayFocusOut", () => { + it("redirects focus out of the overlay list", async () => { + const message = { + command: "redirectOverlayFocusOut", + direction: RedirectFocusDirection.Next, + }; + const redirectOverlayFocusOutSpy = jest.spyOn( + overlayBackground as any, + "redirectOverlayFocusOut", + ); + + sendPortMessage(listPortSpy, message); + await flushPromises(); + + expect(redirectOverlayFocusOutSpy).toHaveBeenCalledWith(message, listPortSpy); + }); + }); + }); + }); +}); diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts new file mode 100644 index 00000000000..1a5d49e9e1f --- /dev/null +++ b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts @@ -0,0 +1,798 @@ +import { firstValueFrom } from "rxjs"; + +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { SHOW_AUTOFILL_BUTTON } from "@bitwarden/common/autofill/constants"; +import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; +import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; + +import { openUnlockPopout } from "../../../auth/popup/utils/auth-popout-window"; +import { BrowserApi } from "../../../platform/browser/browser-api"; +import { + openViewVaultItemPopout, + openAddEditVaultItemPopout, +} from "../../../vault/popup/utils/vault-popout-window"; +import { LockedVaultPendingNotificationsData } from "../../background/abstractions/notification.background"; +import { OverlayBackground as OverlayBackgroundInterface } from "../../background/abstractions/overlay.background"; +import { AutofillOverlayElement, AutofillOverlayPort } from "../../enums/autofill-overlay.enum"; +import { AutofillService, PageDetail } from "../../services/abstractions/autofill.service"; + +import { + FocusedFieldData, + OverlayBackgroundExtensionMessageHandlers, + OverlayButtonPortMessageHandlers, + OverlayCipherData, + OverlayListPortMessageHandlers, + OverlayBackgroundExtensionMessage, + OverlayAddNewItemMessage, + OverlayPortMessage, + WebsiteIconData, +} from "./abstractions/overlay.background.deprecated"; + +class LegacyOverlayBackground implements OverlayBackgroundInterface { + private readonly openUnlockPopout = openUnlockPopout; + private readonly openViewVaultItemPopout = openViewVaultItemPopout; + private readonly openAddEditVaultItemPopout = openAddEditVaultItemPopout; + private overlayLoginCiphers: Map = new Map(); + private pageDetailsForTab: Record< + chrome.runtime.MessageSender["tab"]["id"], + Map + > = {}; + private userAuthStatus: AuthenticationStatus = AuthenticationStatus.LoggedOut; + private overlayButtonPort: chrome.runtime.Port; + private overlayListPort: chrome.runtime.Port; + private expiredPorts: chrome.runtime.Port[] = []; + private focusedFieldData: FocusedFieldData; + private overlayPageTranslations: Record; + private iconsServerUrl: string; + private readonly extensionMessageHandlers: OverlayBackgroundExtensionMessageHandlers = { + openAutofillOverlay: () => this.openOverlay(false), + autofillOverlayElementClosed: ({ message, sender }) => + this.overlayElementClosed(message, sender), + autofillOverlayAddNewVaultItem: ({ message, sender }) => this.addNewVaultItem(message, sender), + getAutofillOverlayVisibility: () => this.getOverlayVisibility(), + checkAutofillOverlayFocused: () => this.checkOverlayFocused(), + focusAutofillOverlayList: () => this.focusOverlayList(), + updateAutofillOverlayPosition: ({ message, sender }) => + this.updateOverlayPosition(message, sender), + updateAutofillOverlayHidden: ({ message }) => this.updateOverlayHidden(message), + updateFocusedFieldData: ({ message, sender }) => this.setFocusedFieldData(message, sender), + collectPageDetailsResponse: ({ message, sender }) => this.storePageDetails(message, sender), + unlockCompleted: ({ message }) => this.unlockCompleted(message), + addedCipher: () => this.updateOverlayCiphers(), + addEditCipherSubmitted: () => this.updateOverlayCiphers(), + editedCipher: () => this.updateOverlayCiphers(), + deletedCipher: () => this.updateOverlayCiphers(), + }; + private readonly overlayButtonPortMessageHandlers: OverlayButtonPortMessageHandlers = { + overlayButtonClicked: ({ port }) => this.handleOverlayButtonClicked(port), + closeAutofillOverlay: ({ port }) => this.closeOverlay(port), + forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true), + overlayPageBlurred: () => this.checkOverlayListFocused(), + redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port), + }; + private readonly overlayListPortMessageHandlers: OverlayListPortMessageHandlers = { + checkAutofillOverlayButtonFocused: () => this.checkOverlayButtonFocused(), + forceCloseAutofillOverlay: ({ port }) => this.closeOverlay(port, true), + overlayPageBlurred: () => this.checkOverlayButtonFocused(), + unlockVault: ({ port }) => this.unlockVault(port), + fillSelectedListItem: ({ message, port }) => this.fillSelectedOverlayListItem(message, port), + addNewVaultItem: ({ port }) => this.getNewVaultItemDetails(port), + viewSelectedCipher: ({ message, port }) => this.viewSelectedCipher(message, port), + redirectOverlayFocusOut: ({ message, port }) => this.redirectOverlayFocusOut(message, port), + }; + + constructor( + private cipherService: CipherService, + private autofillService: AutofillService, + private authService: AuthService, + private environmentService: EnvironmentService, + private domainSettingsService: DomainSettingsService, + private autofillSettingsService: AutofillSettingsServiceAbstraction, + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + private themeStateService: ThemeStateService, + ) {} + + /** + * Removes cached page details for a tab + * based on the passed tabId. + * + * @param tabId - Used to reference the page details of a specific tab + */ + removePageDetails(tabId: number) { + if (!this.pageDetailsForTab[tabId]) { + return; + } + + this.pageDetailsForTab[tabId].clear(); + delete this.pageDetailsForTab[tabId]; + } + + /** + * Sets up the extension message listeners and gets the settings for the + * overlay's visibility and the user's authentication status. + */ + async init() { + this.setupExtensionMessageListeners(); + const env = await firstValueFrom(this.environmentService.environment$); + this.iconsServerUrl = env.getIconsUrl(); + await this.getOverlayVisibility(); + await this.getAuthStatus(); + } + + /** + * Updates the overlay list's ciphers and sends the updated list to the overlay list iframe. + * Queries all ciphers for the given url, and sorts them by last used. Will not update the + * list of ciphers if the extension is not unlocked. + */ + async updateOverlayCiphers() { + const authStatus = await firstValueFrom(this.authService.activeAccountStatus$); + if (authStatus !== AuthenticationStatus.Unlocked) { + return; + } + + const currentTab = await BrowserApi.getTabFromCurrentWindowId(); + if (!currentTab?.url) { + return; + } + + this.overlayLoginCiphers = new Map(); + const ciphersViews = (await this.cipherService.getAllDecryptedForUrl(currentTab.url)).sort( + (a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b), + ); + for (let cipherIndex = 0; cipherIndex < ciphersViews.length; cipherIndex++) { + this.overlayLoginCiphers.set(`overlay-cipher-${cipherIndex}`, ciphersViews[cipherIndex]); + } + + const ciphers = await this.getOverlayCipherData(); + this.overlayListPort?.postMessage({ command: "updateOverlayListCiphers", ciphers }); + await BrowserApi.tabSendMessageData(currentTab, "updateIsOverlayCiphersPopulated", { + isOverlayCiphersPopulated: Boolean(ciphers.length), + }); + } + + /** + * Strips out unnecessary data from the ciphers and returns an array of + * objects that contain the cipher data needed for the overlay list. + */ + private async getOverlayCipherData(): Promise { + const showFavicons = await firstValueFrom(this.domainSettingsService.showFavicons$); + const overlayCiphersArray = Array.from(this.overlayLoginCiphers); + const overlayCipherData: OverlayCipherData[] = []; + let loginCipherIcon: WebsiteIconData; + + for (let cipherIndex = 0; cipherIndex < overlayCiphersArray.length; cipherIndex++) { + const [overlayCipherId, cipher] = overlayCiphersArray[cipherIndex]; + if (!loginCipherIcon && cipher.type === CipherType.Login) { + loginCipherIcon = buildCipherIcon(this.iconsServerUrl, cipher, showFavicons); + } + + overlayCipherData.push({ + id: overlayCipherId, + name: cipher.name, + type: cipher.type, + reprompt: cipher.reprompt, + favorite: cipher.favorite, + icon: + cipher.type === CipherType.Login + ? loginCipherIcon + : buildCipherIcon(this.iconsServerUrl, cipher, showFavicons), + login: cipher.type === CipherType.Login ? { username: cipher.login.username } : null, + card: cipher.type === CipherType.Card ? cipher.card.subTitle : null, + }); + } + + return overlayCipherData; + } + + /** + * Handles aggregation of page details for a tab. Stores the page details + * in association with the tabId of the tab that sent the message. + * + * @param message - Message received from the `collectPageDetailsResponse` command + * @param sender - The sender of the message + */ + private storePageDetails( + message: OverlayBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + const pageDetails = { + frameId: sender.frameId, + tab: sender.tab, + details: message.details, + }; + + const pageDetailsMap = this.pageDetailsForTab[sender.tab.id]; + if (!pageDetailsMap) { + this.pageDetailsForTab[sender.tab.id] = new Map([[sender.frameId, pageDetails]]); + return; + } + + pageDetailsMap.set(sender.frameId, pageDetails); + } + + /** + * Triggers autofill for the selected cipher in the overlay list. Also places + * the selected cipher at the top of the list of ciphers. + * + * @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID. + * @param sender - The sender of the port message + */ + private async fillSelectedOverlayListItem( + { overlayCipherId }: OverlayPortMessage, + { sender }: chrome.runtime.Port, + ) { + const pageDetails = this.pageDetailsForTab[sender.tab.id]; + if (!overlayCipherId || !pageDetails?.size) { + return; + } + + const cipher = this.overlayLoginCiphers.get(overlayCipherId); + + if (await this.autofillService.isPasswordRepromptRequired(cipher, sender.tab)) { + return; + } + const totpCode = await this.autofillService.doAutoFill({ + tab: sender.tab, + cipher: cipher, + pageDetails: Array.from(pageDetails.values()), + fillNewPassword: true, + allowTotpAutofill: true, + }); + + if (totpCode) { + this.platformUtilsService.copyToClipboard(totpCode); + } + + this.overlayLoginCiphers = new Map([[overlayCipherId, cipher], ...this.overlayLoginCiphers]); + } + + /** + * Checks if the overlay is focused. Will check the overlay list + * if it is open, otherwise it will check the overlay button. + */ + private checkOverlayFocused() { + if (this.overlayListPort) { + this.checkOverlayListFocused(); + + return; + } + + this.checkOverlayButtonFocused(); + } + + /** + * Posts a message to the overlay button iframe to check if it is focused. + */ + private checkOverlayButtonFocused() { + this.overlayButtonPort?.postMessage({ command: "checkAutofillOverlayButtonFocused" }); + } + + /** + * Posts a message to the overlay list iframe to check if it is focused. + */ + private checkOverlayListFocused() { + this.overlayListPort?.postMessage({ command: "checkAutofillOverlayListFocused" }); + } + + /** + * Sends a message to the sender tab to close the autofill overlay. + * + * @param sender - The sender of the port message + * @param forceCloseOverlay - Identifies whether the overlay should be force closed + */ + private closeOverlay({ sender }: chrome.runtime.Port, forceCloseOverlay = 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 + BrowserApi.tabSendMessageData(sender.tab, "closeAutofillOverlay", { forceCloseOverlay }); + } + + /** + * Handles cleanup when an overlay element is closed. Disconnects + * the list and button ports and sets them to null. + * + * @param overlayElement - The overlay element that was closed, either the list or button + * @param sender - The sender of the port message + */ + private overlayElementClosed( + { overlayElement }: OverlayBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + if (sender.tab.id !== this.focusedFieldData?.tabId) { + this.expiredPorts.forEach((port) => port.disconnect()); + this.expiredPorts = []; + return; + } + + if (overlayElement === AutofillOverlayElement.Button) { + this.overlayButtonPort?.disconnect(); + this.overlayButtonPort = null; + + return; + } + + this.overlayListPort?.disconnect(); + this.overlayListPort = null; + } + + /** + * Updates the position of either the overlay list or button. The position + * is based on the focused field's position and dimensions. + * + * @param overlayElement - The overlay element to update, either the list or button + * @param sender - The sender of the port message + */ + private updateOverlayPosition( + { overlayElement }: { overlayElement?: string }, + sender: chrome.runtime.MessageSender, + ) { + if (!overlayElement || sender.tab.id !== this.focusedFieldData?.tabId) { + return; + } + + if (overlayElement === AutofillOverlayElement.Button) { + this.overlayButtonPort?.postMessage({ + command: "updateIframePosition", + styles: this.getOverlayButtonPosition(), + }); + + return; + } + + this.overlayListPort?.postMessage({ + command: "updateIframePosition", + styles: this.getOverlayListPosition(), + }); + } + + /** + * Gets the position of the focused field and calculates the position + * of the overlay button based on the focused field's position and dimensions. + */ + private getOverlayButtonPosition() { + if (!this.focusedFieldData) { + return; + } + + const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; + const { paddingRight, paddingLeft } = this.focusedFieldData.focusedFieldStyles; + let elementOffset = height * 0.37; + if (height >= 35) { + elementOffset = height >= 50 ? height * 0.47 : height * 0.42; + } + + const elementHeight = height - elementOffset; + const elementTopPosition = top + elementOffset / 2; + let elementLeftPosition = left + width - height + elementOffset / 2; + + const fieldPaddingRight = parseInt(paddingRight, 10); + const fieldPaddingLeft = parseInt(paddingLeft, 10); + if (fieldPaddingRight > fieldPaddingLeft) { + elementLeftPosition = left + width - height - (fieldPaddingRight - elementOffset + 2); + } + + return { + top: `${Math.round(elementTopPosition)}px`, + left: `${Math.round(elementLeftPosition)}px`, + height: `${Math.round(elementHeight)}px`, + width: `${Math.round(elementHeight)}px`, + }; + } + + /** + * Gets the position of the focused field and calculates the position + * of the overlay list based on the focused field's position and dimensions. + */ + private getOverlayListPosition() { + if (!this.focusedFieldData) { + return; + } + + const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; + return { + width: `${Math.round(width)}px`, + top: `${Math.round(top + height)}px`, + left: `${Math.round(left)}px`, + }; + } + + /** + * Sets the focused field data to the data passed in the extension message. + * + * @param focusedFieldData - Contains the rects and styles of the focused field. + * @param sender - The sender of the extension message + */ + private setFocusedFieldData( + { focusedFieldData }: OverlayBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + ) { + this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id }; + } + + /** + * Updates the overlay's visibility based on the display property passed in the extension message. + * + * @param display - The display property of the overlay, either "block" or "none" + */ + private updateOverlayHidden({ display }: OverlayBackgroundExtensionMessage) { + if (!display) { + return; + } + + const portMessage = { command: "updateOverlayHidden", styles: { display } }; + + this.overlayButtonPort?.postMessage(portMessage); + this.overlayListPort?.postMessage(portMessage); + } + + /** + * Sends a message to the currently active tab to open the autofill overlay. + * + * @param isFocusingFieldElement - Identifies whether the field element should be focused when the overlay is opened + * @param isOpeningFullOverlay - Identifies whether the full overlay should be forced open regardless of other states + */ + private async openOverlay(isFocusingFieldElement = false, isOpeningFullOverlay = false) { + const currentTab = await BrowserApi.getTabFromCurrentWindowId(); + + await BrowserApi.tabSendMessageData(currentTab, "openAutofillOverlay", { + isFocusingFieldElement, + isOpeningFullOverlay, + authStatus: await this.getAuthStatus(), + }); + } + + /** + * Gets the overlay's visibility setting from the settings service. + */ + private async getOverlayVisibility(): Promise { + return await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$); + } + + /** + * Gets the user's authentication status from the auth service. If the user's + * authentication status has changed, the overlay button's authentication status + * will be updated and the overlay list's ciphers will be updated. + */ + private async getAuthStatus() { + const formerAuthStatus = this.userAuthStatus; + this.userAuthStatus = await this.authService.getAuthStatus(); + + if ( + this.userAuthStatus !== formerAuthStatus && + this.userAuthStatus === AuthenticationStatus.Unlocked + ) { + this.updateOverlayButtonAuthStatus(); + await this.updateOverlayCiphers(); + } + + return this.userAuthStatus; + } + + /** + * Sends a message to the overlay button to update its authentication status. + */ + private updateOverlayButtonAuthStatus() { + this.overlayButtonPort?.postMessage({ + command: "updateOverlayButtonAuthStatus", + authStatus: this.userAuthStatus, + }); + } + + /** + * Handles the overlay button being clicked. If the user is not authenticated, + * the vault will be unlocked. If the user is authenticated, the overlay will + * be opened. + * + * @param port - The port of the overlay button + */ + private handleOverlayButtonClicked(port: chrome.runtime.Port) { + if (this.userAuthStatus !== AuthenticationStatus.Unlocked) { + // 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.unlockVault(port); + return; + } + + // 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.openOverlay(false, true); + } + + /** + * Facilitates opening the unlock popout window. + * + * @param port - The port of the overlay list + */ + private async unlockVault(port: chrome.runtime.Port) { + const { sender } = port; + + this.closeOverlay(port); + const retryMessage: LockedVaultPendingNotificationsData = { + commandToRetry: { message: { command: "openAutofillOverlay" }, sender }, + target: "overlay.background", + }; + await BrowserApi.tabSendMessageData( + sender.tab, + "addToLockedVaultPendingNotifications", + retryMessage, + ); + await this.openUnlockPopout(sender.tab, true); + } + + /** + * Triggers the opening of a vault item popout window associated + * with the passed cipher ID. + * @param overlayCipherId - Cipher ID corresponding to the overlayLoginCiphers map. Does not correspond to the actual cipher's ID. + * @param sender - The sender of the port message + */ + private async viewSelectedCipher( + { overlayCipherId }: OverlayPortMessage, + { sender }: chrome.runtime.Port, + ) { + const cipher = this.overlayLoginCiphers.get(overlayCipherId); + if (!cipher) { + return; + } + + await this.openViewVaultItemPopout(sender.tab, { + cipherId: cipher.id, + action: SHOW_AUTOFILL_BUTTON, + }); + } + + /** + * Facilitates redirecting focus to the overlay list. + */ + private focusOverlayList() { + this.overlayListPort?.postMessage({ command: "focusOverlayList" }); + } + + /** + * Updates the authentication status for the user and opens the overlay if + * a followup command is present in the message. + * + * @param message - Extension message received from the `unlockCompleted` command + */ + private async unlockCompleted(message: OverlayBackgroundExtensionMessage) { + await this.getAuthStatus(); + + if (message.data?.commandToRetry?.message?.command === "openAutofillOverlay") { + await this.openOverlay(true); + } + } + + /** + * Gets the translations for the overlay page. + */ + private getTranslations() { + if (!this.overlayPageTranslations) { + this.overlayPageTranslations = { + locale: BrowserApi.getUILanguage(), + opensInANewWindow: this.i18nService.translate("opensInANewWindow"), + buttonPageTitle: this.i18nService.translate("bitwardenOverlayButton"), + toggleBitwardenVaultOverlay: this.i18nService.translate("toggleBitwardenVaultOverlay"), + listPageTitle: this.i18nService.translate("bitwardenVault"), + unlockYourAccount: this.i18nService.translate("unlockYourAccountToViewMatchingLogins"), + unlockAccount: this.i18nService.translate("unlockAccount"), + fillCredentialsFor: this.i18nService.translate("fillCredentialsFor"), + partialUsername: this.i18nService.translate("partialUsername"), + view: this.i18nService.translate("view"), + noItemsToShow: this.i18nService.translate("noItemsToShow"), + newItem: this.i18nService.translate("newItem"), + addNewVaultItem: this.i18nService.translate("addNewVaultItem"), + }; + } + + return this.overlayPageTranslations; + } + + /** + * Facilitates redirecting focus out of one of the + * overlay elements to elements on the page. + * + * @param direction - The direction to redirect focus to (either "next", "previous" or "current) + * @param sender - The sender of the port message + */ + private redirectOverlayFocusOut( + { direction }: OverlayPortMessage, + { sender }: chrome.runtime.Port, + ) { + if (!direction) { + return; + } + + // 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 + BrowserApi.tabSendMessageData(sender.tab, "redirectOverlayFocusOut", { direction }); + } + + /** + * Triggers adding a new vault item from the overlay. Gathers data + * input by the user before calling to open the add/edit window. + * + * @param sender - The sender of the port message + */ + private getNewVaultItemDetails({ sender }: chrome.runtime.Port) { + void BrowserApi.tabSendMessage(sender.tab, { command: "addNewVaultItemFromOverlay" }); + } + + /** + * Handles adding a new vault item from the overlay. Gathers data login + * data captured in the extension message. + * + * @param login - The login data captured from the extension message + * @param sender - The sender of the extension message + */ + private async addNewVaultItem( + { login }: OverlayAddNewItemMessage, + sender: chrome.runtime.MessageSender, + ) { + if (!login) { + return; + } + + const uriView = new LoginUriView(); + uriView.uri = login.uri; + + const loginView = new LoginView(); + loginView.uris = [uriView]; + loginView.username = login.username || ""; + loginView.password = login.password || ""; + + const cipherView = new CipherView(); + cipherView.name = (Utils.getHostname(login.uri) || login.hostname).replace(/^www\./, ""); + cipherView.folderId = null; + cipherView.type = CipherType.Login; + cipherView.login = loginView; + + await this.cipherService.setAddEditCipherInfo({ + cipher: cipherView, + collectionIds: cipherView.collectionIds, + }); + + await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id }); + await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher"); + } + + /** + * Sets up the extension message listeners for the overlay. + */ + private setupExtensionMessageListeners() { + BrowserApi.messageListener("overlay.background", this.handleExtensionMessage); + BrowserApi.addListener(chrome.runtime.onConnect, this.handlePortOnConnect); + } + + /** + * Handles extension messages sent to the extension background. + * + * @param message - The message received from the extension + * @param sender - The sender of the message + * @param sendResponse - The response to send back to the sender + */ + private handleExtensionMessage = ( + message: OverlayBackgroundExtensionMessage, + sender: chrome.runtime.MessageSender, + sendResponse: (response?: any) => void, + ) => { + const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command]; + if (!handler) { + return null; + } + + const messageResponse = handler({ message, sender }); + if (typeof messageResponse === "undefined") { + return null; + } + + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + Promise.resolve(messageResponse).then((response) => sendResponse(response)); + return true; + }; + + /** + * Handles the connection of a port to the extension background. + * + * @param port - The port that connected to the extension background + */ + private handlePortOnConnect = async (port: chrome.runtime.Port) => { + const isOverlayListPort = port.name === AutofillOverlayPort.List; + const isOverlayButtonPort = port.name === AutofillOverlayPort.Button; + if (!isOverlayListPort && !isOverlayButtonPort) { + return; + } + + this.storeOverlayPort(port); + port.onMessage.addListener(this.handleOverlayElementPortMessage); + port.postMessage({ + command: `initAutofillOverlay${isOverlayListPort ? "List" : "Button"}`, + authStatus: await this.getAuthStatus(), + styleSheetUrl: chrome.runtime.getURL(`overlay/${isOverlayListPort ? "list" : "button"}.css`), + theme: await firstValueFrom(this.themeStateService.selectedTheme$), + translations: this.getTranslations(), + ciphers: isOverlayListPort ? await this.getOverlayCipherData() : null, + }); + this.updateOverlayPosition( + { + overlayElement: isOverlayListPort + ? AutofillOverlayElement.List + : AutofillOverlayElement.Button, + }, + port.sender, + ); + }; + + /** + * Stores the connected overlay port and sets up any existing ports to be disconnected. + * + * @param port - The port to store +| */ + private storeOverlayPort(port: chrome.runtime.Port) { + if (port.name === AutofillOverlayPort.List) { + this.storeExpiredOverlayPort(this.overlayListPort); + this.overlayListPort = port; + return; + } + + if (port.name === AutofillOverlayPort.Button) { + this.storeExpiredOverlayPort(this.overlayButtonPort); + this.overlayButtonPort = port; + } + } + + /** + * When registering a new connection, we want to ensure that the port is disconnected. + * This method places an existing port in the expiredPorts array to be disconnected + * at a later time. + * + * @param port - The port to store in the expiredPorts array + */ + private storeExpiredOverlayPort(port: chrome.runtime.Port | null) { + if (port) { + this.expiredPorts.push(port); + } + } + + /** + * Handles messages sent to the overlay list or button ports. + * + * @param message - The message received from the port + * @param port - The port that sent the message + */ + private handleOverlayElementPortMessage = ( + message: OverlayBackgroundExtensionMessage, + port: chrome.runtime.Port, + ) => { + const command = message?.command; + let handler: CallableFunction | undefined; + + if (port.name === AutofillOverlayPort.Button) { + handler = this.overlayButtonPortMessageHandlers[command]; + } + + if (port.name === AutofillOverlayPort.List) { + handler = this.overlayListPortMessageHandlers[command]; + } + + if (!handler) { + return; + } + + handler({ message, port }); + }; +} + +export default LegacyOverlayBackground; diff --git a/apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts b/apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts new file mode 100644 index 00000000000..ed422822b36 --- /dev/null +++ b/apps/browser/src/autofill/deprecated/content/abstractions/autofill-init.deprecated.ts @@ -0,0 +1,41 @@ +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; + +import AutofillScript from "../../../models/autofill-script"; + +type AutofillExtensionMessage = { + command: string; + tab?: chrome.tabs.Tab; + sender?: string; + fillScript?: AutofillScript; + url?: string; + pageDetailsUrl?: string; + ciphers?: any; + data?: { + authStatus?: AuthenticationStatus; + isFocusingFieldElement?: boolean; + isOverlayCiphersPopulated?: boolean; + direction?: "previous" | "next"; + isOpeningFullOverlay?: boolean; + forceCloseOverlay?: boolean; + autofillOverlayVisibility?: number; + }; +}; + +type AutofillExtensionMessageParam = { message: AutofillExtensionMessage }; + +type AutofillExtensionMessageHandlers = { + [key: string]: CallableFunction; + collectPageDetails: ({ message }: AutofillExtensionMessageParam) => void; + collectPageDetailsImmediately: ({ message }: AutofillExtensionMessageParam) => void; + fillForm: ({ message }: AutofillExtensionMessageParam) => void; + openAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void; + closeAutofillOverlay: ({ message }: AutofillExtensionMessageParam) => void; + addNewVaultItemFromOverlay: () => void; + redirectOverlayFocusOut: ({ message }: AutofillExtensionMessageParam) => void; + updateIsOverlayCiphersPopulated: ({ message }: AutofillExtensionMessageParam) => void; + bgUnlockPopoutOpened: () => void; + bgVaultItemRepromptPopoutOpened: () => void; + updateAutofillOverlayVisibility: ({ message }: AutofillExtensionMessageParam) => void; +}; + +export { AutofillExtensionMessage, AutofillExtensionMessageHandlers }; diff --git a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts new file mode 100644 index 00000000000..96d5e85ca34 --- /dev/null +++ b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.spec.ts @@ -0,0 +1,604 @@ +import { mock } from "jest-mock-extended"; + +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; + +import { RedirectFocusDirection } from "../../enums/autofill-overlay.enum"; +import AutofillPageDetails from "../../models/autofill-page-details"; +import AutofillScript from "../../models/autofill-script"; +import { + flushPromises, + mockQuerySelectorAllDefinedCall, + sendMockExtensionMessage, +} from "../../spec/testing-utils"; +import AutofillOverlayContentServiceDeprecated from "../services/autofill-overlay-content.service.deprecated"; + +import { AutofillExtensionMessage } from "./abstractions/autofill-init.deprecated"; +import AutofillInitDeprecated from "./autofill-init.deprecated"; + +describe("AutofillInit", () => { + let autofillInit: AutofillInitDeprecated; + const autofillOverlayContentService = mock(); + const originalDocumentReadyState = document.readyState; + const mockQuerySelectorAll = mockQuerySelectorAllDefinedCall(); + + beforeEach(() => { + chrome.runtime.connect = jest.fn().mockReturnValue({ + onDisconnect: { + addListener: jest.fn(), + }, + }); + autofillInit = new AutofillInitDeprecated(autofillOverlayContentService); + window.IntersectionObserver = jest.fn(() => mock()); + }); + + afterEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + Object.defineProperty(document, "readyState", { + value: originalDocumentReadyState, + writable: true, + }); + }); + + afterAll(() => { + mockQuerySelectorAll.mockRestore(); + }); + + describe("init", () => { + it("sets up the extension message listeners", () => { + jest.spyOn(autofillInit as any, "setupExtensionMessageListeners"); + + autofillInit.init(); + + expect(autofillInit["setupExtensionMessageListeners"]).toHaveBeenCalled(); + }); + + it("triggers a collection of page details if the document is in a `complete` ready state", () => { + jest.useFakeTimers(); + Object.defineProperty(document, "readyState", { value: "complete", writable: true }); + + autofillInit.init(); + jest.advanceTimersByTime(250); + + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith( + { + command: "bgCollectPageDetails", + sender: "autofillInit", + }, + expect.any(Function), + ); + }); + + it("registers a window load listener to collect the page details if the document is not in a `complete` ready state", () => { + jest.spyOn(window, "addEventListener"); + Object.defineProperty(document, "readyState", { value: "loading", writable: true }); + + autofillInit.init(); + + expect(window.addEventListener).toHaveBeenCalledWith("load", expect.any(Function)); + }); + }); + + describe("setupExtensionMessageListeners", () => { + it("sets up a chrome runtime on message listener", () => { + jest.spyOn(chrome.runtime.onMessage, "addListener"); + + autofillInit["setupExtensionMessageListeners"](); + + expect(chrome.runtime.onMessage.addListener).toHaveBeenCalledWith( + autofillInit["handleExtensionMessage"], + ); + }); + }); + + describe("handleExtensionMessage", () => { + let message: AutofillExtensionMessage; + let sender: chrome.runtime.MessageSender; + const sendResponse = jest.fn(); + + beforeEach(() => { + message = { + command: "collectPageDetails", + tab: mock(), + sender: "sender", + }; + sender = mock(); + }); + + it("returns a undefined value if a extension message handler is not found with the given message command", () => { + message.command = "unknownCommand"; + + const response = autofillInit["handleExtensionMessage"](message, sender, sendResponse); + + expect(response).toBe(null); + }); + + it("returns a undefined value if the message handler does not return a response", async () => { + const response1 = await autofillInit["handleExtensionMessage"](message, sender, sendResponse); + await flushPromises(); + + expect(response1).not.toBe(false); + + message.command = "removeAutofillOverlay"; + message.fillScript = mock(); + + const response2 = autofillInit["handleExtensionMessage"](message, sender, sendResponse); + await flushPromises(); + + expect(response2).toBe(null); + }); + + it("returns a true value and calls sendResponse if the message handler returns a response", async () => { + message.command = "collectPageDetailsImmediately"; + const pageDetails: AutofillPageDetails = { + title: "title", + url: "http://example.com", + documentUrl: "documentUrl", + forms: {}, + fields: [], + collectedTimestamp: 0, + }; + jest + .spyOn(autofillInit["collectAutofillContentService"], "getPageDetails") + .mockResolvedValue(pageDetails); + + const response = await autofillInit["handleExtensionMessage"](message, sender, sendResponse); + await flushPromises(); + + expect(response).toBe(true); + expect(sendResponse).toHaveBeenCalledWith(pageDetails); + }); + + describe("extension message handlers", () => { + beforeEach(() => { + autofillInit.init(); + }); + + describe("collectPageDetails", () => { + it("sends the collected page details for autofill using a background script message", async () => { + const pageDetails: AutofillPageDetails = { + title: "title", + url: "http://example.com", + documentUrl: "documentUrl", + forms: {}, + fields: [], + collectedTimestamp: 0, + }; + const message = { + command: "collectPageDetails", + sender: "sender", + tab: mock(), + }; + jest + .spyOn(autofillInit["collectAutofillContentService"], "getPageDetails") + .mockResolvedValue(pageDetails); + + sendMockExtensionMessage(message, sender, sendResponse); + await flushPromises(); + + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ + command: "collectPageDetailsResponse", + tab: message.tab, + details: pageDetails, + sender: message.sender, + }); + }); + }); + + describe("collectPageDetailsImmediately", () => { + it("returns collected page details for autofill if set to send the details in the response", async () => { + const pageDetails: AutofillPageDetails = { + title: "title", + url: "http://example.com", + documentUrl: "documentUrl", + forms: {}, + fields: [], + collectedTimestamp: 0, + }; + jest + .spyOn(autofillInit["collectAutofillContentService"], "getPageDetails") + .mockResolvedValue(pageDetails); + + sendMockExtensionMessage( + { command: "collectPageDetailsImmediately" }, + sender, + sendResponse, + ); + await flushPromises(); + + expect(autofillInit["collectAutofillContentService"].getPageDetails).toHaveBeenCalled(); + expect(sendResponse).toBeCalledWith(pageDetails); + expect(chrome.runtime.sendMessage).not.toHaveBeenCalledWith({ + command: "collectPageDetailsResponse", + tab: message.tab, + details: pageDetails, + sender: message.sender, + }); + }); + }); + + describe("fillForm", () => { + let fillScript: AutofillScript; + beforeEach(() => { + fillScript = mock(); + jest.spyOn(autofillInit["insertAutofillContentService"], "fillForm").mockImplementation(); + }); + + it("skips calling the InsertAutofillContentService and does not fill the form if the url to fill is not equal to the current tab url", async () => { + const fillScript = mock(); + const message = { + command: "fillForm", + fillScript, + pageDetailsUrl: "https://a-different-url.com", + }; + + sendMockExtensionMessage(message); + await flushPromises(); + + expect(autofillInit["insertAutofillContentService"].fillForm).not.toHaveBeenCalledWith( + fillScript, + ); + }); + + it("calls the InsertAutofillContentService to fill the form", async () => { + sendMockExtensionMessage({ + command: "fillForm", + fillScript, + pageDetailsUrl: window.location.href, + }); + await flushPromises(); + + expect(autofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( + fillScript, + ); + }); + + it("removes the overlay when filling the form", async () => { + const blurAndRemoveOverlaySpy = jest.spyOn(autofillInit as any, "blurAndRemoveOverlay"); + sendMockExtensionMessage({ + command: "fillForm", + fillScript, + pageDetailsUrl: window.location.href, + }); + await flushPromises(); + + expect(blurAndRemoveOverlaySpy).toHaveBeenCalled(); + }); + + it("updates the isCurrentlyFilling property of the overlay to true after filling", async () => { + jest.useFakeTimers(); + jest.spyOn(autofillInit as any, "updateOverlayIsCurrentlyFilling"); + jest + .spyOn(autofillInit["autofillOverlayContentService"], "focusMostRecentOverlayField") + .mockImplementation(); + + sendMockExtensionMessage({ + command: "fillForm", + fillScript, + pageDetailsUrl: window.location.href, + }); + await flushPromises(); + jest.advanceTimersByTime(300); + + expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(1, true); + expect(autofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( + fillScript, + ); + expect(autofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith(2, false); + }); + + it("skips attempting to focus the most recent field if the autofillOverlayContentService is not present", async () => { + jest.useFakeTimers(); + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + jest.spyOn(newAutofillInit as any, "updateOverlayIsCurrentlyFilling"); + jest + .spyOn(newAutofillInit["insertAutofillContentService"], "fillForm") + .mockImplementation(); + + sendMockExtensionMessage({ + command: "fillForm", + fillScript, + pageDetailsUrl: window.location.href, + }); + await flushPromises(); + jest.advanceTimersByTime(300); + + expect(newAutofillInit["updateOverlayIsCurrentlyFilling"]).toHaveBeenNthCalledWith( + 1, + true, + ); + expect(newAutofillInit["insertAutofillContentService"].fillForm).toHaveBeenCalledWith( + fillScript, + ); + expect(newAutofillInit["updateOverlayIsCurrentlyFilling"]).not.toHaveBeenNthCalledWith( + 2, + false, + ); + }); + }); + + describe("openAutofillOverlay", () => { + const message = { + command: "openAutofillOverlay", + data: { + isFocusingFieldElement: true, + isOpeningFullOverlay: true, + authStatus: AuthenticationStatus.Unlocked, + }, + }; + + it("skips attempting to open the autofill overlay if the autofillOverlayContentService is not present", () => { + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + + sendMockExtensionMessage(message); + + expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); + }); + + it("opens the autofill overlay", () => { + sendMockExtensionMessage(message); + + expect( + autofillInit["autofillOverlayContentService"].openAutofillOverlay, + ).toHaveBeenCalledWith({ + isFocusingFieldElement: message.data.isFocusingFieldElement, + isOpeningFullOverlay: message.data.isOpeningFullOverlay, + authStatus: message.data.authStatus, + }); + }); + }); + + describe("closeAutofillOverlay", () => { + beforeEach(() => { + autofillInit["autofillOverlayContentService"].isFieldCurrentlyFocused = false; + autofillInit["autofillOverlayContentService"].isCurrentlyFilling = false; + }); + + it("skips attempting to remove the overlay if the autofillOverlayContentService is not present", () => { + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); + + sendMockExtensionMessage({ + command: "closeAutofillOverlay", + data: { forceCloseOverlay: false }, + }); + + expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); + }); + + it("removes the autofill overlay if the message flags a forced closure", () => { + sendMockExtensionMessage({ + command: "closeAutofillOverlay", + data: { forceCloseOverlay: true }, + }); + + expect( + autofillInit["autofillOverlayContentService"].removeAutofillOverlay, + ).toHaveBeenCalled(); + }); + + it("ignores the message if a field is currently focused", () => { + autofillInit["autofillOverlayContentService"].isFieldCurrentlyFocused = true; + + sendMockExtensionMessage({ command: "closeAutofillOverlay" }); + + expect( + autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, + ).not.toHaveBeenCalled(); + expect( + autofillInit["autofillOverlayContentService"].removeAutofillOverlay, + ).not.toHaveBeenCalled(); + }); + + it("removes the autofill overlay list if the overlay is currently filling", () => { + autofillInit["autofillOverlayContentService"].isCurrentlyFilling = true; + + sendMockExtensionMessage({ command: "closeAutofillOverlay" }); + + expect( + autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, + ).toHaveBeenCalled(); + expect( + autofillInit["autofillOverlayContentService"].removeAutofillOverlay, + ).not.toHaveBeenCalled(); + }); + + it("removes the entire overlay if the overlay is not currently filling", () => { + sendMockExtensionMessage({ command: "closeAutofillOverlay" }); + + expect( + autofillInit["autofillOverlayContentService"].removeAutofillOverlayList, + ).not.toHaveBeenCalled(); + expect( + autofillInit["autofillOverlayContentService"].removeAutofillOverlay, + ).toHaveBeenCalled(); + }); + }); + + describe("addNewVaultItemFromOverlay", () => { + it("will not add a new vault item if the autofillOverlayContentService is not present", () => { + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + + sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" }); + + expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); + }); + + it("will add a new vault item", () => { + sendMockExtensionMessage({ command: "addNewVaultItemFromOverlay" }); + + expect(autofillInit["autofillOverlayContentService"].addNewVaultItem).toHaveBeenCalled(); + }); + }); + + describe("redirectOverlayFocusOut", () => { + const message = { + command: "redirectOverlayFocusOut", + data: { + direction: RedirectFocusDirection.Next, + }, + }; + + it("ignores the message to redirect focus if the autofillOverlayContentService does not exist", () => { + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + + sendMockExtensionMessage(message); + + expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); + }); + + it("redirects the overlay focus", () => { + sendMockExtensionMessage(message); + + expect( + autofillInit["autofillOverlayContentService"].redirectOverlayFocusOut, + ).toHaveBeenCalledWith(message.data.direction); + }); + }); + + describe("updateIsOverlayCiphersPopulated", () => { + const message = { + command: "updateIsOverlayCiphersPopulated", + data: { + isOverlayCiphersPopulated: true, + }, + }; + + it("skips updating whether the ciphers are populated if the autofillOverlayContentService does note exist", () => { + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + + sendMockExtensionMessage(message); + + expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); + }); + + it("updates whether the overlay ciphers are populated", () => { + sendMockExtensionMessage(message); + + expect(autofillInit["autofillOverlayContentService"].isOverlayCiphersPopulated).toEqual( + message.data.isOverlayCiphersPopulated, + ); + }); + }); + + describe("bgUnlockPopoutOpened", () => { + it("skips attempting to blur and remove the overlay if the autofillOverlayContentService is not present", () => { + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); + + sendMockExtensionMessage({ command: "bgUnlockPopoutOpened" }); + + expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); + expect(newAutofillInit["removeAutofillOverlay"]).not.toHaveBeenCalled(); + }); + + it("blurs the most recently focused feel and remove the autofill overlay", () => { + jest.spyOn(autofillInit["autofillOverlayContentService"], "blurMostRecentOverlayField"); + jest.spyOn(autofillInit as any, "removeAutofillOverlay"); + + sendMockExtensionMessage({ command: "bgUnlockPopoutOpened" }); + + expect( + autofillInit["autofillOverlayContentService"].blurMostRecentOverlayField, + ).toHaveBeenCalled(); + expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled(); + }); + }); + + describe("bgVaultItemRepromptPopoutOpened", () => { + it("skips attempting to blur and remove the overlay if the autofillOverlayContentService is not present", () => { + const newAutofillInit = new AutofillInitDeprecated(undefined); + newAutofillInit.init(); + jest.spyOn(newAutofillInit as any, "removeAutofillOverlay"); + + sendMockExtensionMessage({ command: "bgVaultItemRepromptPopoutOpened" }); + + expect(newAutofillInit["autofillOverlayContentService"]).toBe(undefined); + expect(newAutofillInit["removeAutofillOverlay"]).not.toHaveBeenCalled(); + }); + + it("blurs the most recently focused feel and remove the autofill overlay", () => { + jest.spyOn(autofillInit["autofillOverlayContentService"], "blurMostRecentOverlayField"); + jest.spyOn(autofillInit as any, "removeAutofillOverlay"); + + sendMockExtensionMessage({ command: "bgVaultItemRepromptPopoutOpened" }); + + expect( + autofillInit["autofillOverlayContentService"].blurMostRecentOverlayField, + ).toHaveBeenCalled(); + expect(autofillInit["removeAutofillOverlay"]).toHaveBeenCalled(); + }); + }); + + describe("updateAutofillOverlayVisibility", () => { + beforeEach(() => { + autofillInit["autofillOverlayContentService"].autofillOverlayVisibility = + AutofillOverlayVisibility.OnButtonClick; + }); + + it("skips attempting to update the overlay visibility if the autofillOverlayVisibility data value is not present", () => { + sendMockExtensionMessage({ + command: "updateAutofillOverlayVisibility", + data: {}, + }); + + expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual( + AutofillOverlayVisibility.OnButtonClick, + ); + }); + + it("updates the overlay visibility value", () => { + const message = { + command: "updateAutofillOverlayVisibility", + data: { + autofillOverlayVisibility: AutofillOverlayVisibility.Off, + }, + }; + + sendMockExtensionMessage(message); + + expect(autofillInit["autofillOverlayContentService"].autofillOverlayVisibility).toEqual( + message.data.autofillOverlayVisibility, + ); + }); + }); + }); + }); + + describe("destroy", () => { + it("clears the timeout used to collect page details on load", () => { + jest.spyOn(window, "clearTimeout"); + + autofillInit.init(); + autofillInit.destroy(); + + expect(window.clearTimeout).toHaveBeenCalledWith( + autofillInit["collectPageDetailsOnLoadTimeout"], + ); + }); + + it("removes the extension message listeners", () => { + autofillInit.destroy(); + + expect(chrome.runtime.onMessage.removeListener).toHaveBeenCalledWith( + autofillInit["handleExtensionMessage"], + ); + }); + + it("destroys the collectAutofillContentService", () => { + jest.spyOn(autofillInit["collectAutofillContentService"], "destroy"); + + autofillInit.destroy(); + + expect(autofillInit["collectAutofillContentService"].destroy).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts new file mode 100644 index 00000000000..b3ee2637b09 --- /dev/null +++ b/apps/browser/src/autofill/deprecated/content/autofill-init.deprecated.ts @@ -0,0 +1,313 @@ +import { AutofillInit } from "../../content/abstractions/autofill-init"; +import AutofillPageDetails from "../../models/autofill-page-details"; +import { CollectAutofillContentService } from "../../services/collect-autofill-content.service"; +import DomElementVisibilityService from "../../services/dom-element-visibility.service"; +import { DomQueryService } from "../../services/dom-query.service"; +import InsertAutofillContentService from "../../services/insert-autofill-content.service"; +import { sendExtensionMessage } from "../../utils"; +import { LegacyAutofillOverlayContentService } from "../services/abstractions/autofill-overlay-content.service"; + +import { + AutofillExtensionMessage, + AutofillExtensionMessageHandlers, +} from "./abstractions/autofill-init.deprecated"; + +class LegacyAutofillInit implements AutofillInit { + private readonly autofillOverlayContentService: LegacyAutofillOverlayContentService | undefined; + private readonly domElementVisibilityService: DomElementVisibilityService; + private readonly collectAutofillContentService: CollectAutofillContentService; + private readonly insertAutofillContentService: InsertAutofillContentService; + private collectPageDetailsOnLoadTimeout: number | NodeJS.Timeout | undefined; + private readonly extensionMessageHandlers: AutofillExtensionMessageHandlers = { + collectPageDetails: ({ message }) => this.collectPageDetails(message), + collectPageDetailsImmediately: ({ message }) => this.collectPageDetails(message, true), + fillForm: ({ message }) => this.fillForm(message), + openAutofillOverlay: ({ message }) => this.openAutofillOverlay(message), + closeAutofillOverlay: ({ message }) => this.removeAutofillOverlay(message), + addNewVaultItemFromOverlay: () => this.addNewVaultItemFromOverlay(), + redirectOverlayFocusOut: ({ message }) => this.redirectOverlayFocusOut(message), + updateIsOverlayCiphersPopulated: ({ message }) => this.updateIsOverlayCiphersPopulated(message), + bgUnlockPopoutOpened: () => this.blurAndRemoveOverlay(), + bgVaultItemRepromptPopoutOpened: () => this.blurAndRemoveOverlay(), + updateAutofillOverlayVisibility: ({ message }) => this.updateAutofillOverlayVisibility(message), + }; + + /** + * AutofillInit constructor. Initializes the DomElementVisibilityService, + * CollectAutofillContentService and InsertAutofillContentService classes. + * + * @param autofillOverlayContentService - The autofill overlay content service, potentially undefined. + */ + constructor(autofillOverlayContentService?: LegacyAutofillOverlayContentService) { + this.autofillOverlayContentService = autofillOverlayContentService; + this.domElementVisibilityService = new DomElementVisibilityService(); + const domQueryService = new DomQueryService(); + this.collectAutofillContentService = new CollectAutofillContentService( + this.domElementVisibilityService, + domQueryService, + this.autofillOverlayContentService, + ); + this.insertAutofillContentService = new InsertAutofillContentService( + this.domElementVisibilityService, + this.collectAutofillContentService, + ); + } + + /** + * Initializes the autofill content script, setting up + * the extension message listeners. This method should + * be called once when the content script is loaded. + */ + init() { + this.setupExtensionMessageListeners(); + this.autofillOverlayContentService?.init(); + this.collectPageDetailsOnLoad(); + } + + /** + * Triggers a collection of the page details from the + * background script, ensuring that autofill is ready + * to act on the page. + */ + private collectPageDetailsOnLoad() { + const sendCollectDetailsMessage = () => { + this.clearCollectPageDetailsOnLoadTimeout(); + this.collectPageDetailsOnLoadTimeout = setTimeout( + () => sendExtensionMessage("bgCollectPageDetails", { sender: "autofillInit" }), + 250, + ); + }; + + if (globalThis.document.readyState === "complete") { + sendCollectDetailsMessage(); + } + + globalThis.addEventListener("load", sendCollectDetailsMessage); + } + + /** + * Collects the page details and sends them to the + * extension background script. If the `sendDetailsInResponse` + * parameter is set to true, the page details will be + * returned to facilitate sending the details in the + * response to the extension message. + * + * @param message - The extension message. + * @param sendDetailsInResponse - Determines whether to send the details in the response. + */ + private async collectPageDetails( + message: AutofillExtensionMessage, + sendDetailsInResponse = false, + ): Promise { + const pageDetails: AutofillPageDetails = + await this.collectAutofillContentService.getPageDetails(); + if (sendDetailsInResponse) { + return pageDetails; + } + + void chrome.runtime.sendMessage({ + command: "collectPageDetailsResponse", + tab: message.tab, + details: pageDetails, + sender: message.sender, + }); + } + + /** + * Fills the form with the given fill script. + * + * @param {AutofillExtensionMessage} message + */ + private async fillForm({ fillScript, pageDetailsUrl }: AutofillExtensionMessage) { + if ((document.defaultView || window).location.href !== pageDetailsUrl) { + return; + } + + this.blurAndRemoveOverlay(); + this.updateOverlayIsCurrentlyFilling(true); + await this.insertAutofillContentService.fillForm(fillScript); + + if (!this.autofillOverlayContentService) { + return; + } + + setTimeout(() => this.updateOverlayIsCurrentlyFilling(false), 250); + } + + /** + * Handles updating the overlay is currently filling value. + * + * @param isCurrentlyFilling - Indicates if the overlay is currently filling + */ + private updateOverlayIsCurrentlyFilling(isCurrentlyFilling: boolean) { + if (!this.autofillOverlayContentService) { + return; + } + + this.autofillOverlayContentService.isCurrentlyFilling = isCurrentlyFilling; + } + + /** + * Opens the autofill overlay. + * + * @param data - The extension message data. + */ + private openAutofillOverlay({ data }: AutofillExtensionMessage) { + if (!this.autofillOverlayContentService) { + return; + } + + this.autofillOverlayContentService.openAutofillOverlay(data); + } + + /** + * Blurs the most recent overlay field and removes the overlay. Used + * in cases where the background unlock or vault item reprompt popout + * is opened. + */ + private blurAndRemoveOverlay() { + if (!this.autofillOverlayContentService) { + return; + } + + this.autofillOverlayContentService.blurMostRecentOverlayField(); + this.removeAutofillOverlay(); + } + + /** + * Removes the autofill overlay if the field is not currently focused. + * If the autofill is currently filling, only the overlay list will be + * removed. + */ + private removeAutofillOverlay(message?: AutofillExtensionMessage) { + if (message?.data?.forceCloseOverlay) { + this.autofillOverlayContentService?.removeAutofillOverlay(); + return; + } + + if ( + !this.autofillOverlayContentService || + this.autofillOverlayContentService.isFieldCurrentlyFocused + ) { + return; + } + + if (this.autofillOverlayContentService.isCurrentlyFilling) { + this.autofillOverlayContentService.removeAutofillOverlayList(); + return; + } + + this.autofillOverlayContentService.removeAutofillOverlay(); + } + + /** + * Adds a new vault item from the overlay. + */ + private addNewVaultItemFromOverlay() { + if (!this.autofillOverlayContentService) { + return; + } + + this.autofillOverlayContentService.addNewVaultItem(); + } + + /** + * Redirects the overlay focus out of an overlay iframe. + * + * @param data - Contains the direction to redirect the focus. + */ + private redirectOverlayFocusOut({ data }: AutofillExtensionMessage) { + if (!this.autofillOverlayContentService) { + return; + } + + this.autofillOverlayContentService.redirectOverlayFocusOut(data?.direction); + } + + /** + * Updates whether the current tab has ciphers that can populate the overlay list + * + * @param data - Contains the isOverlayCiphersPopulated value + * + */ + private updateIsOverlayCiphersPopulated({ data }: AutofillExtensionMessage) { + if (!this.autofillOverlayContentService) { + return; + } + + this.autofillOverlayContentService.isOverlayCiphersPopulated = Boolean( + data?.isOverlayCiphersPopulated, + ); + } + + /** + * Updates the autofill overlay visibility. + * + * @param data - Contains the autoFillOverlayVisibility value + */ + private updateAutofillOverlayVisibility({ data }: AutofillExtensionMessage) { + if (!this.autofillOverlayContentService || isNaN(data?.autofillOverlayVisibility)) { + return; + } + + this.autofillOverlayContentService.autofillOverlayVisibility = data?.autofillOverlayVisibility; + } + + /** + * Clears the send collect details message timeout. + */ + private clearCollectPageDetailsOnLoadTimeout() { + if (this.collectPageDetailsOnLoadTimeout) { + clearTimeout(this.collectPageDetailsOnLoadTimeout); + } + } + + /** + * Sets up the extension message listeners for the content script. + */ + private setupExtensionMessageListeners() { + chrome.runtime.onMessage.addListener(this.handleExtensionMessage); + } + + /** + * Handles the extension messages sent to the content script. + * + * @param message - The extension message. + * @param sender - The message sender. + * @param sendResponse - The send response callback. + */ + private handleExtensionMessage = ( + message: AutofillExtensionMessage, + sender: chrome.runtime.MessageSender, + sendResponse: (response?: any) => void, + ): boolean => { + const command: string = message.command; + const handler: CallableFunction | undefined = this.extensionMessageHandlers[command]; + if (!handler) { + return null; + } + + const messageResponse = handler({ message, sender }); + if (typeof messageResponse === "undefined") { + return null; + } + + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + Promise.resolve(messageResponse).then((response) => sendResponse(response)); + return true; + }; + + /** + * Handles destroying the autofill init content script. Removes all + * listeners, timeouts, and object instances to prevent memory leaks. + */ + destroy() { + this.clearCollectPageDetailsOnLoadTimeout(); + chrome.runtime.onMessage.removeListener(this.handleExtensionMessage); + this.collectAutofillContentService.destroy(); + this.autofillOverlayContentService?.destroy(); + } +} + +export default LegacyAutofillInit; diff --git a/apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts b/apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts new file mode 100644 index 00000000000..66d672172ae --- /dev/null +++ b/apps/browser/src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts @@ -0,0 +1,14 @@ +import { setupAutofillInitDisconnectAction } from "../../utils"; +import LegacyAutofillOverlayContentService from "../services/autofill-overlay-content.service.deprecated"; + +import LegacyAutofillInit from "./autofill-init.deprecated"; + +(function (windowContext) { + if (!windowContext.bitwardenAutofillInit) { + const autofillOverlayContentService = new LegacyAutofillOverlayContentService(); + windowContext.bitwardenAutofillInit = new LegacyAutofillInit(autofillOverlayContentService); + setupAutofillInitDisconnectAction(windowContext); + + windowContext.bitwardenAutofillInit.init(); + } +})(window); diff --git a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-button.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-button.deprecated.ts similarity index 100% rename from apps/browser/src/autofill/overlay/abstractions/autofill-overlay-button.ts rename to apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-button.deprecated.ts diff --git a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-iframe.service.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-iframe.service.deprecated.ts similarity index 100% rename from apps/browser/src/autofill/overlay/abstractions/autofill-overlay-iframe.service.ts rename to apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-iframe.service.deprecated.ts diff --git a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-list.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts similarity index 96% rename from apps/browser/src/autofill/overlay/abstractions/autofill-overlay-list.ts rename to apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts index b656f238dce..83578b13043 100644 --- a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-list.ts +++ b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-list.deprecated.ts @@ -1,6 +1,6 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { OverlayCipherData } from "../../background/abstractions/overlay.background"; +import { OverlayCipherData } from "../../background/abstractions/overlay.background.deprecated"; type OverlayListMessage = { command: string }; diff --git a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-page-element.ts b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts similarity index 89% rename from apps/browser/src/autofill/overlay/abstractions/autofill-overlay-page-element.ts rename to apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts index eb3c2fa4a71..368ae4e7303 100644 --- a/apps/browser/src/autofill/overlay/abstractions/autofill-overlay-page-element.ts +++ b/apps/browser/src/autofill/deprecated/overlay/abstractions/autofill-overlay-page-element.deprecated.ts @@ -1,5 +1,5 @@ -import { OverlayButtonWindowMessageHandlers } from "./autofill-overlay-button"; -import { OverlayListWindowMessageHandlers } from "./autofill-overlay-list"; +import { OverlayButtonWindowMessageHandlers } from "./autofill-overlay-button.deprecated"; +import { OverlayListWindowMessageHandlers } from "./autofill-overlay-list.deprecated"; type WindowMessageHandlers = OverlayButtonWindowMessageHandlers | OverlayListWindowMessageHandlers; diff --git a/apps/browser/src/autofill/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.spec.ts.snap b/apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap similarity index 95% rename from apps/browser/src/autofill/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.spec.ts.snap rename to apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap index cb8e4a541bb..132bd968899 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.spec.ts.snap +++ b/apps/browser/src/autofill/deprecated/overlay/iframe-content/__snapshots__/autofill-overlay-iframe.service.deprecated.spec.ts.snap @@ -15,7 +15,7 @@ exports[`AutofillOverlayIframeService initOverlayIframe sets up the iframe's att `; + }); + + it("returns null if the sub frame URL cannot be parsed correctly", async () => { + delete globalThis.location; + globalThis.location = { href: "invalid-base" } as Location; + sendMockExtensionMessage( + { + command: "getSubFrameOffsets", + subFrameUrl: iframeSource, + }, + mock(), + sendResponseSpy, + ); + await flushPromises(); + + expect(sendResponseSpy).toHaveBeenCalledWith(null); + }); + + it("calculates the sub frame's offsets if a single frame with the referenced url exists", async () => { + sendMockExtensionMessage( + { + command: "getSubFrameOffsets", + subFrameUrl: iframeSource, + }, + mock(), + sendResponseSpy, + ); + await flushPromises(); + + expect(sendResponseSpy).toHaveBeenCalledWith({ + frameId: undefined, + left: 2, + top: 2, + url: iframeSource, + }); + }); + + it("returns null if a matching iframe is not found", async () => { + document.body.innerHTML = ""; + sendMockExtensionMessage( + { + command: "getSubFrameOffsets", + subFrameUrl: iframeSource, + }, + mock(), + sendResponseSpy, + ); + await flushPromises(); + + expect(sendResponseSpy).toHaveBeenCalledWith(null); + }); + + it("returns null if two or more iframes are found with the same src", async () => { + document.body.innerHTML = ` + + + `; + + sendMockExtensionMessage( + { + command: "getSubFrameOffsets", + subFrameUrl: iframeSource, + }, + mock(), + sendResponseSpy, + ); + await flushPromises(); + + expect(sendResponseSpy).toHaveBeenCalledWith(null); + }); + }); + + describe("getSubFrameOffsetsFromWindowMessage", () => { + it("sends a message to the parent to calculate the sub frame positioning", () => { + jest.spyOn(globalThis.parent, "postMessage").mockImplementation(); + const subFrameId = 10; + + sendMockExtensionMessage({ + command: "getSubFrameOffsetsFromWindowMessage", + subFrameId, + }); + + expect(globalThis.parent.postMessage).toHaveBeenCalledWith( + { + command: "calculateSubFramePositioning", + subFrameData: { + url: window.location.href, + frameId: subFrameId, + left: 0, + top: 0, + parentFrameIds: [0], + subFrameDepth: 0, + }, + }, + "*", + ); + }); + + describe("calculateSubFramePositioning", () => { + beforeEach(() => { + autofillOverlayContentService.init(); + jest.spyOn(globalThis.parent, "postMessage"); + document.body.innerHTML = ``; + }); + + it("destroys the inline menu listeners on the origin frame if the depth exceeds the threshold", async () => { + document.body.innerHTML = ``; + const iframe = document.querySelector("iframe") as HTMLIFrameElement; + const subFrameData = { + url: "https://example.com/", + frameId: 10, + left: 0, + top: 0, + parentFrameIds: [1, 2, 3], + subFrameDepth: MAX_SUB_FRAME_DEPTH, + }; + sendExtensionMessageSpy.mockResolvedValue(4); + + postWindowMessage( + { command: "calculateSubFramePositioning", subFrameData }, + "*", + iframe.contentWindow as any, + ); + await flushPromises(); + + expect(globalThis.parent.postMessage).not.toHaveBeenCalled(); + }); + + it("calculates the sub frame offset for the current frame and sends those values to the parent if not in the top frame", async () => { + Object.defineProperty(window, "top", { + value: null, + writable: true, + }); + document.body.innerHTML = ``; + const iframe = document.querySelector("iframe") as HTMLIFrameElement; + const subFrameData = { + url: "https://example.com/", + frameId: 10, + left: 0, + top: 0, + parentFrameIds: [1, 2, 3], + subFrameDepth: 0, + }; + + postWindowMessage( + { command: "calculateSubFramePositioning", subFrameData }, + "*", + iframe.contentWindow as any, + ); + await flushPromises(); + + expect(globalThis.parent.postMessage).toHaveBeenCalledWith( + { + command: "calculateSubFramePositioning", + subFrameData: { + frameId: 10, + left: expect.any(Number), + parentFrameIds: [1, 2, 3], + top: expect.any(Number), + url: "https://example.com/", + subFrameDepth: expect.any(Number), + }, + }, + "*", + ); + }); + + it("posts the calculated sub frame data to the background", async () => { + document.body.innerHTML = ``; + const iframe = document.querySelector("iframe") as HTMLIFrameElement; + const subFrameData = { + url: "https://example.com/", + frameId: 10, + left: 0, + top: 0, + parentFrameIds: [1, 2, 3], + subFrameDepth: expect.any(Number), + }; + + postWindowMessage( + { command: "calculateSubFramePositioning", subFrameData }, + "*", + iframe.contentWindow as any, + ); + await flushPromises(); + + expect(sendExtensionMessageSpy).toHaveBeenCalledWith("updateSubFrameData", { + subFrameData: { + frameId: 10, + left: expect.any(Number), + top: expect.any(Number), + url: "https://example.com/", + parentFrameIds: [1, 2, 3, 4], + subFrameDepth: expect.any(Number), + }, + }); + }); + }); + }); + + describe("checkMostRecentlyFocusedFieldHasValue message handler", () => { + it("returns true if the most recently focused field has a truthy value", async () => { + autofillOverlayContentService["mostRecentlyFocusedField"] = mock< + ElementWithOpId + >({ value: "test" }); + + sendMockExtensionMessage( + { + command: "checkMostRecentlyFocusedFieldHasValue", + }, + mock(), + sendResponseSpy, + ); + await flushPromises(); + + expect(sendResponseSpy).toHaveBeenCalledWith(true); + }); + }); + + describe("setupRebuildSubFrameOffsetsListeners message handler", () => { + let autofillFieldElement: ElementWithOpId; + + beforeEach(() => { + Object.defineProperty(window, "top", { + value: null, + writable: true, + }); + jest.spyOn(globalThis, "addEventListener"); + jest.spyOn(globalThis.document.body, "addEventListener"); + document.body.innerHTML = ` +
+ + +
+ `; + autofillFieldElement = document.getElementById( + "username-field", + ) as ElementWithOpId; + }); + + describe("skipping the setup of the sub frame listeners", () => { + it('skips setup when the window is the "top" frame', async () => { + Object.defineProperty(window, "top", { + value: window, + writable: true, + }); + + sendMockExtensionMessage({ command: "setupRebuildSubFrameOffsetsListeners" }); + await flushPromises(); + + expect(globalThis.addEventListener).not.toHaveBeenCalledWith( + EVENTS.FOCUS, + expect.any(Function), + ); + expect(globalThis.document.body.addEventListener).not.toHaveBeenCalledWith( + EVENTS.MOUSEENTER, + expect.any(Function), + ); + }); + + it("skips setup when no form fields exist on the current frame", async () => { + autofillOverlayContentService["formFieldElements"] = new Map(); + + sendMockExtensionMessage({ command: "setupRebuildSubFrameOffsetsListeners" }); + await flushPromises(); + + expect(globalThis.addEventListener).not.toHaveBeenCalledWith( + EVENTS.FOCUS, + expect.any(Function), + ); + expect(globalThis.document.body.addEventListener).not.toHaveBeenCalledWith( + EVENTS.MOUSEENTER, + expect.any(Function), + ); + }); + }); + + it("sets up the sub frame rebuild listeners when the sub frame contains fields", async () => { + autofillOverlayContentService["formFieldElements"].set( + autofillFieldElement, + createAutofillFieldMock(), + ); + + sendMockExtensionMessage({ command: "setupRebuildSubFrameOffsetsListeners" }); + await flushPromises(); + + expect(globalThis.addEventListener).toHaveBeenCalledWith( + EVENTS.FOCUS, + expect.any(Function), + ); + expect(globalThis.document.body.addEventListener).toHaveBeenCalledWith( + EVENTS.MOUSEENTER, + expect.any(Function), + ); + }); + + describe("triggering the sub frame listener", () => { + beforeEach(async () => { + autofillOverlayContentService["formFieldElements"].set( + autofillFieldElement, + createAutofillFieldMock(), + ); + await sendMockExtensionMessage({ command: "setupRebuildSubFrameOffsetsListeners" }); + }); + + it("triggers a rebuild of the sub frame listener when a focus event occurs", async () => { + globalThis.dispatchEvent(new Event(EVENTS.FOCUS)); + await flushPromises(); + + expect(sendExtensionMessageSpy).toHaveBeenCalledWith("triggerSubFrameFocusInRebuild"); + }); + }); + }); + + describe("destroyAutofillInlineMenuListeners message handler", () => { + it("destroys the inline menu listeners", () => { + jest.spyOn(autofillOverlayContentService, "destroy"); + + sendMockExtensionMessage({ command: "destroyAutofillInlineMenuListeners" }); + + expect(autofillOverlayContentService.destroy).toHaveBeenCalled(); + }); + }); + + describe("getFormFieldDataForNotification message handler", () => { + it("returns early if a field is currently focused", async () => { + jest + .spyOn(autofillOverlayContentService as any, "isFieldCurrentlyFocused") + .mockReturnValue(true); + + sendMockExtensionMessage( + { command: "getFormFieldDataForNotification" }, + mock(), + sendResponseSpy, + ); + await flushPromises(); + + expect(sendResponseSpy).toHaveBeenCalledWith(undefined); + }); + + it("returns the form field data for a notification", async () => { + sendMockExtensionMessage( + { command: "getFormFieldDataForNotification" }, + mock(), + sendResponseSpy, + ); + await flushPromises(); + + expect(sendResponseSpy).toHaveBeenCalledWith({ + uri: globalThis.document.URL, + username: "", + password: "", + newPassword: "", + }); + }); }); }); @@ -1663,43 +2639,25 @@ describe("AutofillOverlayContentService", () => { opid: "password-field", form: "validFormId", elementNumber: 2, - autocompleteType: "current-password", + autoCompleteType: "current-password", type: "password", }); pageDetailsMock = mock({ forms: { validFormId: mock() }, fields: [autofillFieldData, passwordFieldData], }); - void autofillOverlayContentService.setupAutofillOverlayListenerOnField( + void autofillOverlayContentService.setupOverlayListeners( autofillFieldElement, autofillFieldData, pageDetailsMock, ); autofillOverlayContentService["mostRecentlyFocusedField"] = autofillFieldElement; - }); - - it("disconnects all mutation observers", () => { - autofillOverlayContentService["setupMutationObserver"](); - jest.spyOn(autofillOverlayContentService["bodyElementMutationObserver"], "disconnect"); - - autofillOverlayContentService.destroy(); - - expect( - autofillOverlayContentService["bodyElementMutationObserver"].disconnect, - ).toHaveBeenCalled(); - }); - - it("clears the user interaction event timeout", () => { - jest.spyOn(autofillOverlayContentService as any, "clearUserInteractionEventTimeout"); - - autofillOverlayContentService.destroy(); - - expect(autofillOverlayContentService["clearUserInteractionEventTimeout"]).toHaveBeenCalled(); + jest.spyOn(globalThis, "clearTimeout"); + jest.spyOn(globalThis.document, "removeEventListener"); + jest.spyOn(globalThis, "removeEventListener"); }); it("de-registers all global event listeners", () => { - jest.spyOn(globalThis.document, "removeEventListener"); - jest.spyOn(globalThis, "removeEventListener"); jest.spyOn(autofillOverlayContentService as any, "removeOverlayRepositionEventListeners"); autofillOverlayContentService.destroy(); @@ -1739,5 +2697,32 @@ describe("AutofillOverlayContentService", () => { autofillFieldElement, ); }); + + it("clears all existing timeouts", () => { + autofillOverlayContentService["focusInlineMenuListTimeout"] = setTimeout(jest.fn(), 100); + autofillOverlayContentService["closeInlineMenuOnRedirectTimeout"] = setTimeout( + jest.fn(), + 100, + ); + + autofillOverlayContentService.destroy(); + + expect(clearTimeout).toHaveBeenCalledWith( + autofillOverlayContentService["focusInlineMenuListTimeout"], + ); + expect(clearTimeout).toHaveBeenCalledWith( + autofillOverlayContentService["closeInlineMenuOnRedirectTimeout"], + ); + }); + + it("deletes all cached user filled field DOM elements", () => { + autofillOverlayContentService["userFilledFields"] = { + username: autofillFieldElement as FillableFormFieldElement, + }; + + autofillOverlayContentService.destroy(); + + expect(autofillOverlayContentService["userFilledFields"]).toEqual(null); + }); }); }); 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 d56a8a80cc6..23a4fc27000 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -3,71 +3,149 @@ import "lit/polyfill-support.js"; import { FocusableElement, tabbable } from "tabbable"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { EVENTS, AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; +import { + EVENTS, + AutofillOverlayVisibility, + AUTOFILL_OVERLAY_HANDLE_REPOSITION, + AUTOFILL_TRIGGER_FORM_FIELD_SUBMIT, +} from "@bitwarden/common/autofill/constants"; +import { CipherType } from "@bitwarden/common/vault/enums"; -import { FocusedFieldData } from "../background/abstractions/overlay.background"; +import { + FocusedFieldData, + NewCardCipherData, + NewIdentityCipherData, + NewLoginCipherData, + SubFrameOffsetData, +} from "../background/abstractions/overlay.background"; +import { AutofillExtensionMessage } from "../content/abstractions/autofill-init"; +import { AutofillFieldQualifier, AutofillFieldQualifierType } from "../enums/autofill-field.enums"; +import { + AutofillOverlayElement, + MAX_SUB_FRAME_DEPTH, + RedirectFocusDirection, +} from "../enums/autofill-overlay.enum"; import AutofillField from "../models/autofill-field"; import AutofillPageDetails from "../models/autofill-page-details"; -import AutofillOverlayButtonIframe from "../overlay/iframe-content/autofill-overlay-button-iframe"; -import AutofillOverlayListIframe from "../overlay/iframe-content/autofill-overlay-list-iframe"; import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types"; import { elementIsFillableFormField, - generateRandomCustomElementName, + elementIsSelectElement, + getAttributeBoolean, sendExtensionMessage, - setElementStyles, + throttle, } from "../utils"; -import { AutofillOverlayElement, RedirectFocusDirection } from "../utils/autofill-overlay.enum"; import { + AutofillOverlayContentExtensionMessageHandlers, AutofillOverlayContentService as AutofillOverlayContentServiceInterface, - OpenAutofillOverlayOptions, + NotificationFormFieldData, + OpenAutofillInlineMenuOptions, + SubFrameDataFromWindowMessage, } from "./abstractions/autofill-overlay-content.service"; +import { DomQueryService } from "./abstractions/dom-query.service"; +import { InlineMenuFieldQualificationService } from "./abstractions/inline-menu-field-qualifications.service"; import { AutoFillConstants } from "./autofill-constants"; -import { InlineMenuFieldQualificationService } from "./inline-menu-field-qualification.service"; -class AutofillOverlayContentService implements AutofillOverlayContentServiceInterface { - private readonly inlineMenuFieldQualificationService: InlineMenuFieldQualificationService; - isFieldCurrentlyFocused = false; - isCurrentlyFilling = false; - isOverlayCiphersPopulated = false; +export class AutofillOverlayContentService implements AutofillOverlayContentServiceInterface { pageDetailsUpdateRequired = false; - autofillOverlayVisibility: number; - private isFirefoxBrowser = - globalThis.navigator.userAgent.indexOf(" Firefox/") !== -1 || - globalThis.navigator.userAgent.indexOf(" Gecko/") !== -1; - private readonly generateRandomCustomElementName = generateRandomCustomElementName; + inlineMenuVisibility: number; private readonly findTabs = tabbable; private readonly sendExtensionMessage = sendExtensionMessage; - private formFieldElements: Set> = new Set([]); - private ignoredFieldTypes: Set = new Set(AutoFillConstants.ExcludedOverlayTypes); + private formFieldElements: Map, AutofillField> = new Map(); + private hiddenFormFieldElements: WeakMap, AutofillField> = + new WeakMap(); + private formElements: Set = new Set(); + private submitElements: Set = new Set(); + private fieldsWithSubmitElements: WeakMap = new WeakMap(); + private ignoredFieldTypes: Set = new Set(AutoFillConstants.ExcludedInlineMenuTypes); private userFilledFields: Record = {}; private authStatus: AuthenticationStatus; private focusableElements: FocusableElement[] = []; - private isOverlayButtonVisible = false; - private isOverlayListVisible = false; - private overlayButtonElement: HTMLElement; - private overlayListElement: HTMLElement; private mostRecentlyFocusedField: ElementWithOpId; private focusedFieldData: FocusedFieldData; - private userInteractionEventTimeout: number | NodeJS.Timeout; - private overlayElementsMutationObserver: MutationObserver; - private bodyElementMutationObserver: MutationObserver; - private documentElementMutationObserver: MutationObserver; - private mutationObserverIterations = 0; - private mutationObserverIterationsResetTimeout: number | NodeJS.Timeout; - private autofillFieldKeywordsMap: WeakMap = new WeakMap(); + private closeInlineMenuOnRedirectTimeout: number | NodeJS.Timeout; + private focusInlineMenuListTimeout: number | NodeJS.Timeout; private eventHandlersMemo: { [key: string]: EventListener } = {}; - private readonly customElementDefaultStyles: Partial = { - all: "initial", - position: "fixed", - display: "block", - zIndex: "2147483647", + private readonly extensionMessageHandlers: AutofillOverlayContentExtensionMessageHandlers = { + openAutofillInlineMenu: ({ message }) => this.openInlineMenu(message), + addNewVaultItemFromOverlay: ({ message }) => this.addNewVaultItem(message), + blurMostRecentlyFocusedField: () => this.blurMostRecentlyFocusedField(), + unsetMostRecentlyFocusedField: () => this.unsetMostRecentlyFocusedField(), + checkIsMostRecentlyFocusedFieldWithinViewport: () => + this.checkIsMostRecentlyFocusedFieldWithinViewport(), + bgUnlockPopoutOpened: () => this.blurMostRecentlyFocusedField(true), + bgVaultItemRepromptPopoutOpened: () => this.blurMostRecentlyFocusedField(true), + redirectAutofillInlineMenuFocusOut: ({ message }) => + this.redirectInlineMenuFocusOut(message?.data?.direction), + updateAutofillInlineMenuVisibility: ({ message }) => this.updateInlineMenuVisibility(message), + getSubFrameOffsets: ({ message }) => this.getSubFrameOffsets(message), + getSubFrameOffsetsFromWindowMessage: ({ message }) => + this.getSubFrameOffsetsFromWindowMessage(message), + checkMostRecentlyFocusedFieldHasValue: () => this.mostRecentlyFocusedFieldHasValue(), + setupRebuildSubFrameOffsetsListeners: () => this.setupRebuildSubFrameOffsetsListeners(), + destroyAutofillInlineMenuListeners: () => this.destroy(), + getFormFieldDataForNotification: () => this.handleGetFormFieldDataForNotificationMessage(), + }; + private readonly loginFieldQualifiers: Record = { + [AutofillFieldQualifier.username]: this.inlineMenuFieldQualificationService.isUsernameField, + [AutofillFieldQualifier.password]: + this.inlineMenuFieldQualificationService.isCurrentPasswordField, + }; + private readonly cardFieldQualifiers: Record = { + [AutofillFieldQualifier.cardholderName]: + this.inlineMenuFieldQualificationService.isFieldForCardholderName, + [AutofillFieldQualifier.cardNumber]: + this.inlineMenuFieldQualificationService.isFieldForCardNumber, + [AutofillFieldQualifier.cardExpirationMonth]: + this.inlineMenuFieldQualificationService.isFieldForCardExpirationMonth, + [AutofillFieldQualifier.cardExpirationYear]: + this.inlineMenuFieldQualificationService.isFieldForCardExpirationYear, + [AutofillFieldQualifier.cardExpirationDate]: + this.inlineMenuFieldQualificationService.isFieldForCardExpirationDate, + [AutofillFieldQualifier.cardCvv]: this.inlineMenuFieldQualificationService.isFieldForCardCvv, + }; + private readonly identityFieldQualifiers: Record = { + [AutofillFieldQualifier.identityTitle]: + this.inlineMenuFieldQualificationService.isFieldForIdentityTitle, + [AutofillFieldQualifier.identityFirstName]: + this.inlineMenuFieldQualificationService.isFieldForIdentityFirstName, + [AutofillFieldQualifier.identityMiddleName]: + this.inlineMenuFieldQualificationService.isFieldForIdentityMiddleName, + [AutofillFieldQualifier.identityLastName]: + this.inlineMenuFieldQualificationService.isFieldForIdentityLastName, + [AutofillFieldQualifier.identityFullName]: + this.inlineMenuFieldQualificationService.isFieldForIdentityFullName, + [AutofillFieldQualifier.identityAddress1]: + this.inlineMenuFieldQualificationService.isFieldForIdentityAddress1, + [AutofillFieldQualifier.identityAddress2]: + this.inlineMenuFieldQualificationService.isFieldForIdentityAddress2, + [AutofillFieldQualifier.identityAddress3]: + this.inlineMenuFieldQualificationService.isFieldForIdentityAddress3, + [AutofillFieldQualifier.identityCity]: + this.inlineMenuFieldQualificationService.isFieldForIdentityCity, + [AutofillFieldQualifier.identityState]: + this.inlineMenuFieldQualificationService.isFieldForIdentityState, + [AutofillFieldQualifier.identityPostalCode]: + this.inlineMenuFieldQualificationService.isFieldForIdentityPostalCode, + [AutofillFieldQualifier.identityCountry]: + this.inlineMenuFieldQualificationService.isFieldForIdentityCountry, + [AutofillFieldQualifier.identityCompany]: + this.inlineMenuFieldQualificationService.isFieldForIdentityCompany, + [AutofillFieldQualifier.identityPhone]: + this.inlineMenuFieldQualificationService.isFieldForIdentityPhone, + [AutofillFieldQualifier.identityEmail]: + this.inlineMenuFieldQualificationService.isFieldForIdentityEmail, + [AutofillFieldQualifier.identityUsername]: + this.inlineMenuFieldQualificationService.isFieldForIdentityUsername, + [AutofillFieldQualifier.newPassword]: + this.inlineMenuFieldQualificationService.isNewPasswordField, }; - constructor() { - this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService(); - } + constructor( + private domQueryService: DomQueryService, + private inlineMenuFieldQualificationService: InlineMenuFieldQualificationService, + ) {} /** * Initializes the autofill overlay content service by setting up the mutation observers. @@ -83,14 +161,22 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte } /** - * Sets up the autofill overlay listener on the form field element. This method is called + * Getter used to access the extension message handlers associated + * with the autofill overlay content service. + */ + get messageHandlers(): AutofillOverlayContentExtensionMessageHandlers { + return this.extensionMessageHandlers; + } + + /** + * Sets up the autofill inline menu listener on the form field element. This method is called * during the page details collection process. * * @param formFieldElement - Form field elements identified during the page details collection process. * @param autofillFieldData - Autofill field data captured from the form field element. * @param pageDetails - The collected page details from the tab. */ - async setupAutofillOverlayListenerOnField( + async setupOverlayListeners( formFieldElement: ElementWithOpId, autofillFieldData: AutofillField, pageDetails: AutofillPageDetails, @@ -102,49 +188,36 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte return; } - this.formFieldElements.add(formFieldElement); - - if (!this.autofillOverlayVisibility) { - await this.getAutofillOverlayVisibility(); - } - - this.setupFormFieldElementEventListeners(formFieldElement); - - if (this.getRootNodeActiveElement(formFieldElement) === formFieldElement) { - await this.triggerFormFieldFocusedAction(formFieldElement); + if (this.isHiddenField(formFieldElement, autofillFieldData)) { return; } - if (!this.mostRecentlyFocusedField) { - await this.updateMostRecentlyFocusedField(formFieldElement); - } + await this.setupOverlayListenersOnQualifiedField(formFieldElement, autofillFieldData); } /** - * Handles opening the autofill overlay. Will conditionally open - * the overlay based on the current autofill overlay visibility setting. - * Allows you to optionally focus the field element when opening the overlay. - * Will also optionally ignore the overlay visibility setting and open the + * Handles opening the autofill inline menu. Will conditionally open + * the inline menu based on the current inline menu visibility setting. + * Allows you to optionally focus the field element when opening the inline menu. + * Will also optionally ignore the inline menu visibility setting and open the * - * @param options - Options for opening the autofill overlay. + * @param options - Options for opening the autofill inline menu. */ - openAutofillOverlay(options: OpenAutofillOverlayOptions = {}) { - const { isFocusingFieldElement, isOpeningFullOverlay, authStatus } = options; + openInlineMenu(options: OpenAutofillInlineMenuOptions = {}) { + const { isFocusingFieldElement, isOpeningFullInlineMenu, authStatus } = options; if (!this.mostRecentlyFocusedField) { return; } if (this.pageDetailsUpdateRequired) { - // 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.sendExtensionMessage("bgCollectPageDetails", { + void this.sendExtensionMessage("bgCollectPageDetails", { sender: "autofillOverlayContentService", }); this.pageDetailsUpdateRequired = false; } if (isFocusingFieldElement && !this.recentlyFocusedFieldIsCurrentlyFocused()) { - this.focusMostRecentOverlayField(); + this.focusMostRecentlyFocusedField(); } if (typeof authStatus !== "undefined") { @@ -152,109 +225,120 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte } if ( - this.autofillOverlayVisibility === AutofillOverlayVisibility.OnButtonClick && - !isOpeningFullOverlay + this.inlineMenuVisibility === AutofillOverlayVisibility.OnButtonClick && + !isOpeningFullInlineMenu ) { - this.updateOverlayButtonPosition(); + this.updateInlineMenuButtonPosition(); return; } - this.updateOverlayElementsPosition(); + this.updateInlineMenuElementsPosition(); } /** * Focuses the most recently focused field element. */ - focusMostRecentOverlayField() { + focusMostRecentlyFocusedField() { this.mostRecentlyFocusedField?.focus(); } /** * Removes focus from the most recently focused field element. */ - blurMostRecentOverlayField() { + blurMostRecentlyFocusedField(isClosingInlineMenu: boolean = false) { this.mostRecentlyFocusedField?.blur(); + + if (isClosingInlineMenu) { + void this.sendExtensionMessage("closeAutofillInlineMenu"); + } } /** - * Removes the autofill overlay from the page. This will initially - * unobserve the body element to ensure the mutation observer no - * longer triggers. + * Sets the most recently focused field within the current frame to a `null` value. */ - removeAutofillOverlay = () => { - this.removeBodyElementObserver(); - this.removeAutofillOverlayButton(); - this.removeAutofillOverlayList(); - }; - - /** - * Removes the overlay button from the DOM if it is currently present. Will - * also remove the overlay reposition event listeners. - */ - removeAutofillOverlayButton() { - if (!this.overlayButtonElement) { - return; - } - - this.overlayButtonElement.remove(); - this.isOverlayButtonVisible = false; - void this.sendExtensionMessage("autofillOverlayElementClosed", { - overlayElement: AutofillOverlayElement.Button, - }); - this.removeOverlayRepositionEventListeners(); - } - - /** - * Removes the overlay list from the DOM if it is currently present. - */ - removeAutofillOverlayList() { - if (!this.overlayListElement) { - return; - } - - this.overlayListElement.remove(); - this.isOverlayListVisible = false; - void this.sendExtensionMessage("autofillOverlayElementClosed", { - overlayElement: AutofillOverlayElement.List, - }); + unsetMostRecentlyFocusedField() { + this.mostRecentlyFocusedField = null; } /** * Formats any found user filled fields for a login cipher and sends a message * to the background script to add a new cipher. */ - addNewVaultItem() { - if (!this.isOverlayListVisible) { + async addNewVaultItem({ addNewCipherType }: AutofillExtensionMessage) { + const command = "autofillOverlayAddNewVaultItem"; + const password = + this.userFilledFields["newPassword"]?.value || this.userFilledFields["password"]?.value; + + if (addNewCipherType === CipherType.Login) { + const login: NewLoginCipherData = { + username: this.userFilledFields["username"]?.value || "", + password: password || "", + uri: globalThis.document.URL, + hostname: globalThis.document.location.hostname, + }; + + void this.sendExtensionMessage(command, { addNewCipherType, login }); + return; } - const login = { - username: this.userFilledFields["username"]?.value || "", - password: this.userFilledFields["password"]?.value || "", - uri: globalThis.document.URL, - hostname: globalThis.document.location.hostname, - }; + if (addNewCipherType === CipherType.Card) { + const card: NewCardCipherData = { + cardholderName: this.userFilledFields["cardholderName"]?.value || "", + number: this.userFilledFields["cardNumber"]?.value || "", + expirationMonth: this.userFilledFields["cardExpirationMonth"]?.value || "", + expirationYear: this.userFilledFields["cardExpirationYear"]?.value || "", + expirationDate: this.userFilledFields["cardExpirationDate"]?.value || "", + cvv: this.userFilledFields["cardCvv"]?.value || "", + }; - // 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.sendExtensionMessage("autofillOverlayAddNewVaultItem", { login }); + void this.sendExtensionMessage(command, { addNewCipherType, card }); + + return; + } + + if (addNewCipherType === CipherType.Identity) { + const identity: NewIdentityCipherData = { + title: this.userFilledFields["identityTitle"]?.value || "", + firstName: this.userFilledFields["identityFirstName"]?.value || "", + middleName: this.userFilledFields["identityMiddleName"]?.value || "", + lastName: this.userFilledFields["identityLastName"]?.value || "", + fullName: this.userFilledFields["identityFullName"]?.value || "", + address1: this.userFilledFields["identityAddress1"]?.value || "", + address2: this.userFilledFields["identityAddress2"]?.value || "", + address3: this.userFilledFields["identityAddress3"]?.value || "", + city: this.userFilledFields["identityCity"]?.value || "", + state: this.userFilledFields["identityState"]?.value || "", + postalCode: this.userFilledFields["identityPostalCode"]?.value || "", + country: this.userFilledFields["identityCountry"]?.value || "", + company: this.userFilledFields["identityCompany"]?.value || "", + phone: this.userFilledFields["identityPhone"]?.value || "", + email: this.userFilledFields["identityEmail"]?.value || "", + username: this.userFilledFields["identityUsername"]?.value || "", + }; + + void this.sendExtensionMessage(command, { addNewCipherType, identity }); + } } /** - * Redirects the keyboard focus out of the overlay, selecting the element that is + * Redirects the keyboard focus out of the inline menu, selecting the element that is * either previous or next in the tab order. If the direction is current, the most * recently focused field will be focused. * - * @param direction - The direction to redirect the focus. + * @param direction - The direction to redirect the focus out. */ - redirectOverlayFocusOut(direction: string) { - if (!this.isOverlayListVisible || !this.mostRecentlyFocusedField) { + private async redirectInlineMenuFocusOut(direction?: string) { + if (!direction || !this.mostRecentlyFocusedField || !(await this.isInlineMenuListVisible())) { return; } if (direction === RedirectFocusDirection.Current) { - this.focusMostRecentOverlayField(); - setTimeout(this.removeAutofillOverlay, 100); + this.focusMostRecentlyFocusedField(); + this.closeInlineMenuOnRedirectTimeout = globalThis.setTimeout( + () => void this.sendExtensionMessage("closeAutofillInlineMenu"), + 100, + ); return; } @@ -268,38 +352,48 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte const indexOffset = direction === RedirectFocusDirection.Previous ? -1 : 1; const redirectFocusElement = this.focusableElements[focusedElementIndex + indexOffset]; - redirectFocusElement?.focus(); + if (redirectFocusElement) { + redirectFocusElement.focus(); + return; + } + + this.focusMostRecentlyFocusedField(); } /** * Sets up the event listeners that facilitate interaction with the form field elements. * Will clear any cached form field element handlers that are encountered when setting - * up a form field element to the overlay. + * up a form field element. * * @param formFieldElement - The form field element to set up the event listeners for. */ private setupFormFieldElementEventListeners(formFieldElement: ElementWithOpId) { this.removeCachedFormFieldEventListeners(formFieldElement); - formFieldElement.addEventListener(EVENTS.BLUR, this.handleFormFieldBlurEvent); - formFieldElement.addEventListener(EVENTS.KEYUP, this.handleFormFieldKeyupEvent); formFieldElement.addEventListener( EVENTS.INPUT, this.handleFormFieldInputEvent(formFieldElement), ); - formFieldElement.addEventListener( - EVENTS.CLICK, - this.handleFormFieldClickEvent(formFieldElement), - ); formFieldElement.addEventListener( EVENTS.FOCUS, this.handleFormFieldFocusEvent(formFieldElement), ); + + if (elementIsSelectElement(formFieldElement)) { + return; + } + + formFieldElement.addEventListener(EVENTS.BLUR, this.handleFormFieldBlurEvent); + formFieldElement.addEventListener(EVENTS.KEYUP, this.handleFormFieldKeyupEvent); + formFieldElement.addEventListener( + EVENTS.CLICK, + this.handleFormFieldClickEvent(formFieldElement), + ); } /** * Removes any cached form field element handlers that are encountered - * when setting up a form field element to present the overlay. + * when setting up a form field element to present the inline menu. * * @param formFieldElement - The form field element to remove the cached handlers for. */ @@ -318,6 +412,214 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte } } + /** + * Sets up listeners on the submit button that triggers a submission of the field's form. + * + * @param formFieldElement - The form field element to set up the submit button listeners for. + * @param autofillFieldData - Autofill field data captured from the form field element. + */ + private setupFormSubmissionEventListeners( + formFieldElement: ElementWithOpId, + autofillFieldData: AutofillField, + ) { + if ( + !elementIsFillableFormField(formFieldElement) || + autofillFieldData.filledByCipherType === CipherType.Card + ) { + return; + } + + if (autofillFieldData.form) { + this.setupSubmitListenerOnFieldWithForms(formFieldElement); + return; + } + + this.setupSubmitListenerOnFormlessField(formFieldElement); + } + + /** + * Sets up the submit listener on the form field element that contains a form element. + * Will establish on submit event listeners on the form element and click listeners on + * the submit button element that triggers the submission of the form. + * + * @param formFieldElement - The form field element to set up the submit listener for. + */ + private setupSubmitListenerOnFieldWithForms(formFieldElement: FillableFormFieldElement) { + const formElement = formFieldElement.form; + if (formElement && !this.formElements.has(formElement)) { + this.formElements.add(formElement); + formElement.addEventListener(EVENTS.SUBMIT, this.handleFormFieldSubmitEvent); + + const closesSubmitButton = this.findSubmitButton(formElement); + this.setupSubmitButtonEventListeners(closesSubmitButton); + } + } + + /** + * Sets up the submit listener on the form field element that does not contain a form element. + * Will establish a submit button event listener on the closest formless submit button element. + * + * @param formFieldElement - The form field element to set up the submit listener for. + */ + private setupSubmitListenerOnFormlessField(formFieldElement: FillableFormFieldElement) { + if (formFieldElement && !this.fieldsWithSubmitElements.has(formFieldElement)) { + const closesSubmitButton = this.findClosestFormlessSubmitButton(formFieldElement); + this.setupSubmitButtonEventListeners(closesSubmitButton); + } + } + + /** + * Finds the closest formless submit button element to the form field element. + * + * @param formFieldElement - The form field element to find the closest formless submit button for. + */ + private findClosestFormlessSubmitButton( + formFieldElement: FillableFormFieldElement, + ): HTMLElement | null { + let currentElement: HTMLElement = formFieldElement; + + while (currentElement && currentElement.tagName !== "HTML") { + const submitButton = this.findSubmitButton(currentElement); + if (submitButton) { + this.formFieldElements.forEach((_, element) => { + if (currentElement.contains(element)) { + this.fieldsWithSubmitElements.set(element as FillableFormFieldElement, submitButton); + } + }); + + return submitButton; + } + + if (!currentElement.parentElement && currentElement.getRootNode() instanceof ShadowRoot) { + currentElement = (currentElement.getRootNode() as ShadowRoot).host as any; + continue; + } + + currentElement = currentElement.parentElement; + } + + return null; + } + + /** + * Finds the submit button element within the provided element. Will attempt to find a generic + * submit element before attempting to find a button or button-like element. + * + * @param element - The element to find the submit button within. + */ + private findSubmitButton(element: HTMLElement): HTMLElement | null { + const genericSubmitElement = this.querySubmitButtonElement(element, "[type='submit']"); + if (genericSubmitElement) { + return genericSubmitElement; + } + + const submitButtonElement = this.querySubmitButtonElement(element, "button, [type='button']"); + if (submitButtonElement) { + return submitButtonElement; + } + } + + /** + * Queries the provided element for a submit button element using the provided selector. + * + * @param element - The element to query for a submit button. + * @param selector - The selector to use to query the element for a submit button. + */ + private querySubmitButtonElement(element: HTMLElement, selector: string) { + const submitButtonElements = this.domQueryService.deepQueryElements( + element, + selector, + ); + for (let index = 0; index < submitButtonElements.length; index++) { + const submitElement = submitButtonElements[index]; + if (this.isElementSubmitButton(submitElement)) { + return submitElement; + } + } + } + + /** + * Determines if the provided element is a submit button element. + * + * @param element - The element to determine if it is a submit button. + */ + private isElementSubmitButton(element: HTMLElement) { + return ( + this.inlineMenuFieldQualificationService.isElementLoginSubmitButton(element) || + this.inlineMenuFieldQualificationService.isElementChangePasswordSubmitButton(element) + ); + } + + /** + * Sets up the event listeners that trigger an indication that a form has been submitted. + * + * @param submitButton - The submit button element to set up the event listeners for. + */ + private setupSubmitButtonEventListeners = (submitButton: HTMLElement) => { + if (!submitButton || this.submitElements.has(submitButton)) { + return; + } + + this.submitElements.add(submitButton); + + const handler = this.useEventHandlersMemo( + throttle(this.handleSubmitButtonInteraction, 150), + AUTOFILL_TRIGGER_FORM_FIELD_SUBMIT, + ); + submitButton.addEventListener(EVENTS.KEYUP, handler); + globalThis.document.addEventListener(EVENTS.CLICK, handler); + globalThis.document.addEventListener(EVENTS.MOUSEUP, handler); + }; + + /** + * Handles click and keyup events that trigger behavior for a submit button element. + * + * @param event - The event that triggered the submit button interaction. + */ + private handleSubmitButtonInteraction = (event: PointerEvent) => { + if ( + !this.submitElements.has(event.target as HTMLElement) || + (event.type === "keyup" && + !["Enter", "Space"].includes((event as unknown as KeyboardEvent).code)) + ) { + return; + } + + this.handleFormFieldSubmitEvent(); + }; + + /** + * Handles the repositioning of the autofill overlay when the form is submitted. + */ + private handleFormFieldSubmitEvent = () => { + void this.sendExtensionMessage("formFieldSubmitted", this.getFormFieldDataForNotification()); + }; + + /** + * Handles capturing the form field data for a notification message. Is triggered from the + * background script when a POST request is encountered. Will not trigger this behavior + * in the case where the user is still typing in the field. + */ + private handleGetFormFieldDataForNotificationMessage = async () => { + if (await this.isFieldCurrentlyFocused()) { + return; + } + + return this.getFormFieldDataForNotification(); + }; + + /** + * Returns the form field data used for add login and change password notifications. + */ + private getFormFieldDataForNotification = (): NotificationFormFieldData => { + return { + uri: globalThis.document.URL, + username: this.userFilledFields["username"]?.value || "", + password: this.userFilledFields["password"]?.value || "", + newPassword: this.userFilledFields["newPassword"]?.value || "", + }; + }; + /** * Helper method that facilitates registration of an event handler to a form field element. * @@ -343,33 +645,35 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte /** * Form Field blur event handler. Updates the value identifying whether - * the field is focused and sends a message to check if the overlay itself + * the field is focused and sends a message to check if the inline menu itself * is currently focused. */ private handleFormFieldBlurEvent = () => { - this.isFieldCurrentlyFocused = 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.sendExtensionMessage("checkAutofillOverlayFocused"); + void this.sendExtensionMessage("updateIsFieldCurrentlyFocused", { + isFieldCurrentlyFocused: false, + }); + void this.sendExtensionMessage("checkAutofillInlineMenuFocused"); }; /** * Form field keyup event handler. Facilitates the ability to remove the - * autofill overlay using the escape key, focusing the overlay list using - * the ArrowDown key, and ensuring that the overlay is repositioned when + * autofill inline menu using the escape key, focusing the inline menu list using + * the ArrowDown key, and ensuring that the inline menu is repositioned when * the form is submitted using the Enter key. * * @param event - The keyup event. */ - private handleFormFieldKeyupEvent = (event: KeyboardEvent) => { + private handleFormFieldKeyupEvent = async (event: globalThis.KeyboardEvent) => { const eventCode = event.code; if (eventCode === "Escape") { - this.removeAutofillOverlay(); + void this.sendExtensionMessage("closeAutofillInlineMenu", { + forceCloseInlineMenu: true, + }); return; } - if (eventCode === "Enter" && !this.isCurrentlyFilling) { - this.handleOverlayRepositionEvent(); + if (eventCode === "Enter" && !(await this.isFieldCurrentlyFilling())) { + void this.handleOverlayRepositionEvent(); return; } @@ -377,28 +681,28 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte event.preventDefault(); event.stopPropagation(); - // 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.focusOverlayList(); + void this.focusInlineMenuList(); } }; /** - * Triggers a focus of the overlay list, if it is visible. If the list is not visible, - * the overlay will be opened and the list will be focused after a short delay. Ensures - * that the overlay list is focused when the user presses the down arrow key. + * Triggers a focus of the inline menu list, if it is visible. If the list is not visible, + * the inline menu will be opened and the list will be focused after a short delay. Ensures + * that the inline menu list is focused when the user presses the down arrow key. */ - private async focusOverlayList() { - if (!this.isOverlayListVisible && this.mostRecentlyFocusedField) { + private async focusInlineMenuList() { + if (this.mostRecentlyFocusedField && !(await this.isInlineMenuListVisible())) { + this.clearFocusInlineMenuListTimeout(); await this.updateMostRecentlyFocusedField(this.mostRecentlyFocusedField); - this.openAutofillOverlay({ isOpeningFullOverlay: true }); - setTimeout(() => this.sendExtensionMessage("focusAutofillOverlayList"), 125); + this.openInlineMenu({ isOpeningFullInlineMenu: true }); + this.focusInlineMenuListTimeout = globalThis.setTimeout( + () => this.sendExtensionMessage("focusAutofillInlineMenuList"), + 125, + ); return; } - // 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.sendExtensionMessage("focusAutofillOverlayList"); + void this.sendExtensionMessage("focusAutofillInlineMenuList"); } /** @@ -416,23 +720,29 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte /** * Triggers when the form field element receives an input event. This method will * store the modified form element data for use when the user attempts to add a new - * vault item. It also acts to remove the overlay list while the user is typing. + * vault item. It also acts to remove the inline menu list while the user is typing. * * @param formFieldElement - The form field element that triggered the input event. */ - private triggerFormFieldInput(formFieldElement: ElementWithOpId) { + private async triggerFormFieldInput(formFieldElement: ElementWithOpId) { if (!elementIsFillableFormField(formFieldElement)) { return; } this.storeModifiedFormElement(formFieldElement); - - if (formFieldElement.value && (this.isOverlayCiphersPopulated || !this.isUserAuthed())) { - this.removeAutofillOverlayList(); + if (elementIsSelectElement(formFieldElement)) { return; } - this.openAutofillOverlay(); + if (await this.hideInlineMenuListOnFilledField(formFieldElement)) { + void this.sendExtensionMessage("closeAutofillInlineMenu", { + overlayElement: AutofillOverlayElement.List, + forceCloseInlineMenu: true, + }); + return; + } + + this.openInlineMenu(); } /** @@ -444,16 +754,104 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte * @private */ private storeModifiedFormElement(formFieldElement: ElementWithOpId) { - if (formFieldElement === this.mostRecentlyFocusedField) { - this.mostRecentlyFocusedField = formFieldElement; + if (formFieldElement !== this.mostRecentlyFocusedField) { + void this.updateMostRecentlyFocusedField(formFieldElement); } - if (formFieldElement.type === "password") { - this.userFilledFields.password = formFieldElement; + const autofillFieldData = this.formFieldElements.get(formFieldElement); + if (!autofillFieldData) { return; } - this.userFilledFields.username = formFieldElement; + if (!autofillFieldData.fieldQualifier) { + switch (autofillFieldData.filledByCipherType) { + case CipherType.Login: + this.qualifyUserFilledLoginField(autofillFieldData); + break; + case CipherType.Card: + this.qualifyUserFilledCardField(autofillFieldData); + break; + case CipherType.Identity: + this.qualifyUserFilledIdentityField(autofillFieldData); + break; + } + } + + this.storeQualifiedUserFilledField(formFieldElement, autofillFieldData); + } + + /** + * Handles qualifying the user field login field to be used when adding a new vault item. + * + * @param autofillFieldData - Autofill field data captured from the form field element. + */ + private qualifyUserFilledLoginField(autofillFieldData: AutofillField) { + for (const [fieldQualifier, fieldQualifierFunction] of Object.entries( + this.loginFieldQualifiers, + )) { + if (fieldQualifierFunction(autofillFieldData)) { + autofillFieldData.fieldQualifier = fieldQualifier as AutofillFieldQualifierType; + return; + } + } + } + + /** + * Handles qualifying the user field card field to be used when adding a new vault item. + * + * @param autofillFieldData - Autofill field data captured from the form field element. + */ + private qualifyUserFilledCardField(autofillFieldData: AutofillField) { + for (const [fieldQualifier, fieldQualifierFunction] of Object.entries( + this.cardFieldQualifiers, + )) { + if (fieldQualifierFunction(autofillFieldData)) { + autofillFieldData.fieldQualifier = fieldQualifier as AutofillFieldQualifierType; + return; + } + } + } + + /** + * Handles qualifying the user field identity field to be used when adding a new vault item. + * + * @param autofillFieldData - Autofill field data captured from the form field element. + */ + private qualifyUserFilledIdentityField(autofillFieldData: AutofillField) { + for (const [fieldQualifier, fieldQualifierFunction] of Object.entries( + this.identityFieldQualifiers, + )) { + if (fieldQualifierFunction(autofillFieldData)) { + autofillFieldData.fieldQualifier = fieldQualifier as AutofillFieldQualifierType; + return; + } + } + } + + /** + * Stores the qualified user filled filed to allow for referencing its value when adding a new vault item. + * + * @param formFieldElement - The form field element that triggered the input event. + * @param autofillFieldData - Autofill field data captured from the form field element. + */ + private storeQualifiedUserFilledField( + formFieldElement: ElementWithOpId, + autofillFieldData: AutofillField, + ) { + if (!autofillFieldData.fieldQualifier) { + return; + } + + const clonedNode = formFieldElement.cloneNode() as FillableFormFieldElement; + const identityLoginFields: AutofillFieldQualifierType[] = [ + AutofillFieldQualifier.identityUsername, + AutofillFieldQualifier.identityEmail, + ]; + if (identityLoginFields.includes(autofillFieldData.fieldQualifier)) { + this.userFilledFields[AutofillFieldQualifier.username] = clonedNode; + } + + this.userFilledFields[autofillFieldData.fieldQualifier] = clonedNode; } /** @@ -470,12 +868,12 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte /** * Triggers when the form field element receives a click event. This method will - * trigger the focused action for the form field element if the overlay is not visible. + * trigger the focused action for the form field element if the inline menu is not visible. * * @param formFieldElement - The form field element that triggered the click event. */ private async triggerFormFieldClickedAction(formFieldElement: ElementWithOpId) { - if (this.isOverlayButtonVisible || this.isOverlayListVisible) { + if ((await this.isInlineMenuButtonVisible()) || (await this.isInlineMenuListVisible())) { return; } @@ -496,37 +894,48 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte /** * Triggers when the form field element receives a focus event. This method will - * update the most recently focused field and open the autofill overlay if the + * update the most recently focused field and open the autofill inline menu if the * autofill process is not currently active. * * @param formFieldElement - The form field element that triggered the focus event. */ private async triggerFormFieldFocusedAction(formFieldElement: ElementWithOpId) { - if (this.isCurrentlyFilling) { + if (await this.isFieldCurrentlyFilling()) { return; } - this.isFieldCurrentlyFocused = true; - this.clearUserInteractionEventTimeout(); + if (elementIsSelectElement(formFieldElement)) { + await this.sendExtensionMessage("closeAutofillInlineMenu", { + forceCloseInlineMenu: true, + }); + return; + } + + await this.sendExtensionMessage("updateIsFieldCurrentlyFocused", { + isFieldCurrentlyFocused: true, + }); const initiallyFocusedField = this.mostRecentlyFocusedField; await this.updateMostRecentlyFocusedField(formFieldElement); - const formElementHasValue = Boolean((formFieldElement as HTMLInputElement).value); + const hideInlineMenuListOnFilledField = await this.hideInlineMenuListOnFilledField( + formFieldElement as FillableFormFieldElement, + ); if ( - this.autofillOverlayVisibility === AutofillOverlayVisibility.OnButtonClick || - (formElementHasValue && initiallyFocusedField !== this.mostRecentlyFocusedField) + this.inlineMenuVisibility === AutofillOverlayVisibility.OnButtonClick || + (initiallyFocusedField !== this.mostRecentlyFocusedField && hideInlineMenuListOnFilledField) ) { - this.removeAutofillOverlayList(); + await this.sendExtensionMessage("closeAutofillInlineMenu", { + overlayElement: AutofillOverlayElement.List, + forceCloseInlineMenu: true, + }); } - if (!formElementHasValue || (!this.isOverlayCiphersPopulated && this.isUserAuthed())) { - // 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.sendExtensionMessage("openAutofillOverlay"); + if (hideInlineMenuListOnFilledField) { + this.updateInlineMenuButtonPosition(); return; } - this.updateOverlayButtonPosition(); + void this.sendExtensionMessage("openAutofillInlineMenu"); } /** @@ -547,82 +956,33 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte } /** - * Updates the position of both the overlay button and overlay list. + * Updates the position of both the inline menu button and list. */ - private updateOverlayElementsPosition() { - this.updateOverlayButtonPosition(); - this.updateOverlayListPosition(); + private updateInlineMenuElementsPosition() { + this.updateInlineMenuButtonPosition(); + this.updateInlineMenuListPosition(); } /** - * Updates the position of the overlay button. + * Updates the position of the inline menu button. */ - private updateOverlayButtonPosition() { - if (!this.overlayButtonElement) { - this.createAutofillOverlayButton(); - this.updateCustomElementDefaultStyles(this.overlayButtonElement); - } - - if (!this.isOverlayButtonVisible) { - this.appendOverlayElementToBody(this.overlayButtonElement); - this.isOverlayButtonVisible = true; - this.setOverlayRepositionEventListeners(); - } - // 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.sendExtensionMessage("updateAutofillOverlayPosition", { + private updateInlineMenuButtonPosition() { + void this.sendExtensionMessage("updateAutofillInlineMenuPosition", { overlayElement: AutofillOverlayElement.Button, }); } /** - * Updates the position of the overlay list. + * Updates the position of the inline menu list. */ - private updateOverlayListPosition() { - if (!this.overlayListElement) { - this.createAutofillOverlayList(); - this.updateCustomElementDefaultStyles(this.overlayListElement); - } - - if (!this.isOverlayListVisible) { - this.appendOverlayElementToBody(this.overlayListElement); - this.isOverlayListVisible = true; - } - - // 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.sendExtensionMessage("updateAutofillOverlayPosition", { + private updateInlineMenuListPosition() { + void this.sendExtensionMessage("updateAutofillInlineMenuPosition", { overlayElement: AutofillOverlayElement.List, }); } /** - * Appends the overlay element to the body element. This method will also - * observe the body element to ensure that the overlay element is not - * interfered with by any DOM changes. - * - * @param element - The overlay element to append to the body element. - */ - private appendOverlayElementToBody(element: HTMLElement) { - this.observeBodyElement(); - globalThis.document.body.appendChild(element); - } - - /** - * Sends a message that facilitates hiding the overlay elements. - * - * @param isHidden - Indicates if the overlay elements should be hidden. - */ - private toggleOverlayHidden(isHidden: boolean) { - const displayValue = isHidden ? "none" : "block"; - void this.sendExtensionMessage("updateAutofillOverlayHidden", { display: displayValue }); - - this.isOverlayButtonVisible = !!this.overlayButtonElement && !isHidden; - this.isOverlayListVisible = !!this.overlayListElement && !isHidden; - } - - /** - * Updates the data used to position the overlay elements in relation + * Updates the data used to position the inline menu elements in relation * to the most recently focused form field. * * @param formFieldElement - The form field element that triggered the focus event. @@ -630,18 +990,42 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte private async updateMostRecentlyFocusedField( formFieldElement: ElementWithOpId, ) { + if ( + !formFieldElement || + !elementIsFillableFormField(formFieldElement) || + elementIsSelectElement(formFieldElement) + ) { + return; + } + this.mostRecentlyFocusedField = formFieldElement; const { paddingRight, paddingLeft } = globalThis.getComputedStyle(formFieldElement); const { width, height, top, left } = await this.getMostRecentlyFocusedFieldRects(formFieldElement); + const autofillFieldData = this.formFieldElements.get(formFieldElement); + let accountCreationFieldType = null; + if ( + (autofillFieldData?.showInlineMenuAccountCreation || + autofillFieldData?.filledByCipherType === CipherType.Login) && + this.inlineMenuFieldQualificationService.isUsernameField(autofillFieldData) + ) { + accountCreationFieldType = this.inlineMenuFieldQualificationService.isEmailField( + autofillFieldData, + ) + ? "email" + : autofillFieldData.type; + } + this.focusedFieldData = { focusedFieldStyles: { paddingRight, paddingLeft }, focusedFieldRects: { width, height, top, left }, + filledByCipherType: autofillFieldData?.filledByCipherType, + showInlineMenuAccountCreation: autofillFieldData?.showInlineMenuAccountCreation, + showPasskeys: !!autofillFieldData?.showPasskeys, + accountCreationFieldType, }; - // 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.sendExtensionMessage("updateFocusedFieldData", { + await this.sendExtensionMessage("updateFocusedFieldData", { focusedFieldData: this.focusedFieldData, }); } @@ -701,7 +1085,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte } /** - * Identifies if the field should have the autofill overlay setup on it. Currently, this is mainly + * Identifies if the field should have the autofill inline menu setup on it. Currently, this is mainly * determined by whether the field correlates with a login cipher. This method will need to be * updated in the future to support other types of forms. * @@ -712,370 +1096,250 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte autofillFieldData: AutofillField, pageDetails: AutofillPageDetails, ): boolean { - if ( - autofillFieldData.readonly || - autofillFieldData.disabled || - !autofillFieldData.viewable || - this.ignoredFieldTypes.has(autofillFieldData.type) - ) { + if (this.ignoredFieldTypes.has(autofillFieldData.type)) { return true; } - return !this.inlineMenuFieldQualificationService.isFieldForLoginForm( - autofillFieldData, - pageDetails, - ); + if ( + this.inlineMenuFieldQualificationService.isFieldForLoginForm(autofillFieldData, pageDetails) + ) { + autofillFieldData.filledByCipherType = CipherType.Login; + autofillFieldData.showPasskeys = autofillFieldData.autoCompleteType.includes("webauthn"); + return false; + } + + if ( + this.inlineMenuFieldQualificationService.isFieldForCreditCardForm( + autofillFieldData, + pageDetails, + ) + ) { + autofillFieldData.filledByCipherType = CipherType.Card; + return false; + } + + if ( + this.inlineMenuFieldQualificationService.isFieldForAccountCreationForm( + autofillFieldData, + pageDetails, + ) + ) { + autofillFieldData.filledByCipherType = CipherType.Identity; + autofillFieldData.showInlineMenuAccountCreation = true; + return false; + } + + if ( + this.inlineMenuFieldQualificationService.isFieldForIdentityForm( + autofillFieldData, + pageDetails, + ) + ) { + autofillFieldData.filledByCipherType = CipherType.Identity; + return false; + } + + return true; } /** - * Creates the autofill overlay button element. Will not attempt - * to create the element if it already exists in the DOM. - */ - private createAutofillOverlayButton() { - if (this.overlayButtonElement) { - return; - } - - if (this.isFirefoxBrowser) { - this.overlayButtonElement = globalThis.document.createElement("div"); - new AutofillOverlayButtonIframe(this.overlayButtonElement); - - return; - } - - const customElementName = this.generateRandomCustomElementName(); - globalThis.customElements?.define( - customElementName, - class extends HTMLElement { - constructor() { - super(); - new AutofillOverlayButtonIframe(this); - } - }, - ); - this.overlayButtonElement = globalThis.document.createElement(customElementName); - } - - /** - * Creates the autofill overlay list element. Will not attempt - * to create the element if it already exists in the DOM. - */ - private createAutofillOverlayList() { - if (this.overlayListElement) { - return; - } - - if (this.isFirefoxBrowser) { - this.overlayListElement = globalThis.document.createElement("div"); - new AutofillOverlayListIframe(this.overlayListElement); - - return; - } - - const customElementName = this.generateRandomCustomElementName(); - globalThis.customElements?.define( - customElementName, - class extends HTMLElement { - constructor() { - super(); - new AutofillOverlayListIframe(this); - } - }, - ); - this.overlayListElement = globalThis.document.createElement(customElementName); - } - - /** - * Updates the default styles for the custom element. This method will - * remove any styles that are added to the custom element by other methods. + * Validates whether a field is considered to be "hidden" based on the field's attributes. + * If the field is hidden, a fallback listener will be set up to ensure that the + * field will have the inline menu set up on it when it becomes visible. * - * @param element - The custom element to update the default styles for. + * @param formFieldElement - The form field element that triggered the focus event. + * @param autofillFieldData - Autofill field data captured from the form field element. */ - private updateCustomElementDefaultStyles(element: HTMLElement) { - this.unobserveCustomElements(); + private isHiddenField( + formFieldElement: ElementWithOpId, + autofillFieldData: AutofillField, + ): boolean { + if (!autofillFieldData.readonly && !autofillFieldData.disabled && autofillFieldData.viewable) { + this.removeHiddenFieldFallbackListener(formFieldElement); + return false; + } - setElementStyles(element, this.customElementDefaultStyles, true); - - this.observeCustomElements(); + this.setupHiddenFieldFallbackListener(formFieldElement, autofillFieldData); + return true; } /** - * Queries the background script for the autofill overlay visibility setting. + * Sets up a fallback listener that will facilitate setting up the + * inline menu on the field when it becomes visible and focused. + * + * @param formFieldElement - The form field element that triggered the focus event. + * @param autofillFieldData - Autofill field data captured from the form field element. + */ + private setupHiddenFieldFallbackListener( + formFieldElement: ElementWithOpId, + autofillFieldData: AutofillField, + ) { + this.hiddenFormFieldElements.set(formFieldElement, autofillFieldData); + formFieldElement.addEventListener(EVENTS.FOCUS, this.handleHiddenFieldFocusEvent); + formFieldElement.addEventListener(EVENTS.INPUT, this.handleHiddenFieldInputEvent); + } + + /** + * Removes the fallback listener that facilitates setting up the inline + * menu on the field when it becomes visible and focused. + * + * @param formFieldElement - The form field element that triggered the focus event. + */ + private removeHiddenFieldFallbackListener(formFieldElement: ElementWithOpId) { + formFieldElement.removeEventListener(EVENTS.FOCUS, this.handleHiddenFieldFocusEvent); + formFieldElement.removeEventListener(EVENTS.INPUT, this.handleHiddenFieldInputEvent); + this.hiddenFormFieldElements.delete(formFieldElement); + } + + /** + * Handles the focus event on a hidden field. When + * triggered, the inline menu is set up on the field. + * + * @param event - The focus event. + */ + private handleHiddenFieldFocusEvent = (event: FocusEvent) => { + const formFieldElement = event.target as ElementWithOpId; + this.handleHiddenElementFallbackEvent(formFieldElement); + }; + + /** + * Handles an input event on a hidden field. When triggered, the inline menu is set up on the + * field. We also capture the input value for the field to facilitate presentation of the value + * for the field in the notification bar. + * + * @param event - The input event. + */ + private handleHiddenFieldInputEvent = async (event: InputEvent) => { + const formFieldElement = event.target as ElementWithOpId; + this.handleHiddenElementFallbackEvent(formFieldElement); + await this.triggerFormFieldInput(formFieldElement); + }; + + /** + * Handles updating the hidden element when a fallback event is triggered. + * + * @param formFieldElement - The form field element that triggered the focus event. + */ + private handleHiddenElementFallbackEvent = ( + formFieldElement: ElementWithOpId, + ) => { + const autofillFieldData = this.hiddenFormFieldElements.get(formFieldElement); + if (autofillFieldData) { + autofillFieldData.readonly = getAttributeBoolean(formFieldElement, "disabled"); + autofillFieldData.disabled = getAttributeBoolean(formFieldElement, "disabled"); + autofillFieldData.viewable = true; + void this.setupOverlayListenersOnQualifiedField(formFieldElement, autofillFieldData); + } + + this.removeHiddenFieldFallbackListener(formFieldElement); + }; + + /** + * Sets up the inline menu on a qualified form field element. + * + * @param formFieldElement - The form field element to set up the inline menu on. + * @param autofillFieldData - Autofill field data captured from the form field element. + */ + private async setupOverlayListenersOnQualifiedField( + formFieldElement: ElementWithOpId, + autofillFieldData: AutofillField, + ) { + this.formFieldElements.set(formFieldElement, autofillFieldData); + + if (!this.mostRecentlyFocusedField) { + await this.updateMostRecentlyFocusedField(formFieldElement); + } + + if (!this.inlineMenuVisibility) { + await this.getInlineMenuVisibility(); + } + + this.setupFormFieldElementEventListeners(formFieldElement); + this.setupFormSubmissionEventListeners(formFieldElement, autofillFieldData); + + if ( + globalThis.document.hasFocus() && + this.getRootNodeActiveElement(formFieldElement) === formFieldElement + ) { + await this.triggerFormFieldFocusedAction(formFieldElement); + } + } + + /** + * Queries the background script for the autofill inline menu visibility setting. * If the setting is not found, a default value of OnFieldFocus will be used * @private */ - private async getAutofillOverlayVisibility() { - const overlayVisibility = await this.sendExtensionMessage("getAutofillOverlayVisibility"); - this.autofillOverlayVisibility = overlayVisibility || AutofillOverlayVisibility.OnFieldFocus; + private async getInlineMenuVisibility() { + const inlineMenuVisibility = await this.sendExtensionMessage("getAutofillInlineMenuVisibility"); + this.inlineMenuVisibility = inlineMenuVisibility || AutofillOverlayVisibility.OnFieldFocus; } /** - * Sets up event listeners that facilitate repositioning - * the autofill overlay on scroll or resize. - */ - private setOverlayRepositionEventListeners() { - globalThis.addEventListener(EVENTS.SCROLL, this.handleOverlayRepositionEvent, { - capture: true, - }); - globalThis.addEventListener(EVENTS.RESIZE, this.handleOverlayRepositionEvent); - } - - /** - * Removes the listeners that facilitate repositioning - * the autofill overlay on scroll or resize. - */ - private removeOverlayRepositionEventListeners() { - globalThis.removeEventListener(EVENTS.SCROLL, this.handleOverlayRepositionEvent, { - capture: true, - }); - globalThis.removeEventListener(EVENTS.RESIZE, this.handleOverlayRepositionEvent); - } - - /** - * Handles the resize or scroll events that enact - * repositioning of the overlay. - */ - private handleOverlayRepositionEvent = () => { - if (!this.isOverlayButtonVisible && !this.isOverlayListVisible) { - return; - } - - this.toggleOverlayHidden(true); - this.clearUserInteractionEventTimeout(); - this.userInteractionEventTimeout = setTimeout( - this.triggerOverlayRepositionUpdates, - 750, - ) as unknown as number; - }; - - /** - * Triggers the overlay reposition updates. This method ensures that the overlay elements - * are correctly positioned when the viewport scrolls or repositions. - */ - private triggerOverlayRepositionUpdates = async () => { - if (!this.recentlyFocusedFieldIsCurrentlyFocused()) { - this.toggleOverlayHidden(false); - this.removeAutofillOverlay(); - return; - } - - await this.updateMostRecentlyFocusedField(this.mostRecentlyFocusedField); - this.updateOverlayElementsPosition(); - this.toggleOverlayHidden(false); - this.clearUserInteractionEventTimeout(); - - if ( - this.focusedFieldData.focusedFieldRects?.top > 0 && - this.focusedFieldData.focusedFieldRects?.top < globalThis.innerHeight - ) { - return; - } - - this.removeAutofillOverlay(); - }; - - /** - * Clears the user interaction event timeout. This is used to ensure that - * the overlay is not repositioned while the user is interacting with it. - */ - private clearUserInteractionEventTimeout() { - if (this.userInteractionEventTimeout) { - clearTimeout(this.userInteractionEventTimeout); - } - } - - /** - * Sets up global event listeners and the mutation - * observer to facilitate required changes to the - * overlay elements. - */ - private setupGlobalEventListeners = () => { - globalThis.document.addEventListener(EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent); - globalThis.addEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent); - this.setupMutationObserver(); - }; - - /** - * Handles the visibility change event. This method will remove the - * autofill overlay if the document is not visible. - */ - private handleVisibilityChangeEvent = () => { - if (document.visibilityState === "visible") { - return; - } - - this.mostRecentlyFocusedField = null; - this.removeAutofillOverlay(); - }; - - /** - * Sets up mutation observers for the overlay elements, the body element, and the - * document element. The mutation observers are used to remove any styles that are - * added to the overlay elements by the website. They are also used to ensure that - * the overlay elements are always present at the bottom of the body element. - */ - private setupMutationObserver = () => { - this.overlayElementsMutationObserver = new MutationObserver( - this.handleOverlayElementMutationObserverUpdate, - ); - - this.bodyElementMutationObserver = new MutationObserver( - this.handleBodyElementMutationObserverUpdate, - ); - }; - - /** - * Sets up mutation observers to verify that the overlay - * elements are not modified by the website. - */ - private observeCustomElements() { - if (this.overlayButtonElement) { - this.overlayElementsMutationObserver?.observe(this.overlayButtonElement, { - attributes: true, - }); - } - - if (this.overlayListElement) { - this.overlayElementsMutationObserver?.observe(this.overlayListElement, { attributes: true }); - } - } - - /** - * Disconnects the mutation observers that are used to verify that the overlay - * elements are not modified by the website. - */ - private unobserveCustomElements() { - this.overlayElementsMutationObserver?.disconnect(); - } - - /** - * Sets up a mutation observer for the body element. The mutation observer is used - * to ensure that the overlay elements are always present at the bottom of the body - * element. - */ - private observeBodyElement() { - this.bodyElementMutationObserver?.observe(globalThis.document.body, { childList: true }); - } - - /** - * Disconnects the mutation observer for the body element. - */ - private removeBodyElementObserver() { - this.bodyElementMutationObserver?.disconnect(); - } - - /** - * Handles the mutation observer update for the overlay elements. This method will - * remove any attributes or styles that might be added to the overlay elements by - * a separate process within the website where this script is injected. + * Returns a value that indicates if we should hide the inline menu list due to a filled field. * - * @param mutationRecord - The mutation record that triggered the update. + * @param formFieldElement - The form field element that triggered the focus event. */ - private handleOverlayElementMutationObserverUpdate = (mutationRecord: MutationRecord[]) => { - if (this.isTriggeringExcessiveMutationObserverIterations()) { - return; - } - - for (let recordIndex = 0; recordIndex < mutationRecord.length; recordIndex++) { - const record = mutationRecord[recordIndex]; - if (record.type !== "attributes") { - continue; - } - - const element = record.target as HTMLElement; - if (record.attributeName !== "style") { - this.removeModifiedElementAttributes(element); - - continue; - } - - element.removeAttribute("style"); - this.updateCustomElementDefaultStyles(element); - } - }; + private async hideInlineMenuListOnFilledField( + formFieldElement?: FillableFormFieldElement, + ): Promise { + return ( + formFieldElement?.value && + ((await this.isInlineMenuCiphersPopulated()) || !this.isUserAuthed()) + ); + } /** - * Removes all elements from a passed overlay - * element except for the style attribute. - * - * @param element - The element to remove the attributes from. + * Indicates whether the most recently focused field has a value. */ - private removeModifiedElementAttributes(element: HTMLElement) { - const attributes = Array.from(element.attributes); - for (let attributeIndex = 0; attributeIndex < attributes.length; attributeIndex++) { - const attribute = attributes[attributeIndex]; - if (attribute.name === "style") { - continue; - } + private mostRecentlyFocusedFieldHasValue() { + return Boolean((this.mostRecentlyFocusedField as FillableFormFieldElement)?.value); + } - element.removeAttribute(attribute.name); + /** + * Updates the local reference to the inline menu visibility setting. + * + * @param data - The data object from the extension message. + */ + private updateInlineMenuVisibility({ data }: AutofillExtensionMessage) { + if (!isNaN(data?.inlineMenuVisibility)) { + this.inlineMenuVisibility = data.inlineMenuVisibility; } } /** - * Handles the mutation observer update for the body element. This method will - * ensure that the overlay elements are always present at the bottom of the body - * element. + * Checks if a field is currently filling within an frame in the tab. */ - private handleBodyElementMutationObserverUpdate = () => { - if ( - (!this.overlayButtonElement && !this.overlayListElement) || - this.isTriggeringExcessiveMutationObserverIterations() - ) { - return; - } - - const lastChild = globalThis.document.body.lastElementChild; - const secondToLastChild = lastChild?.previousElementSibling; - const lastChildIsOverlayList = lastChild === this.overlayListElement; - const lastChildIsOverlayButton = lastChild === this.overlayButtonElement; - const secondToLastChildIsOverlayButton = secondToLastChild === this.overlayButtonElement; - - if ( - (lastChildIsOverlayList && secondToLastChildIsOverlayButton) || - (lastChildIsOverlayButton && !this.isOverlayListVisible) - ) { - return; - } - - if ( - (lastChildIsOverlayList && !secondToLastChildIsOverlayButton) || - (lastChildIsOverlayButton && this.isOverlayListVisible) - ) { - globalThis.document.body.insertBefore(this.overlayButtonElement, this.overlayListElement); - return; - } - - globalThis.document.body.insertBefore(lastChild, this.overlayButtonElement); - }; + private async isFieldCurrentlyFilling() { + return (await this.sendExtensionMessage("checkIsFieldCurrentlyFilling")) === true; + } /** - * Identifies if the mutation observer is triggering excessive iterations. - * Will trigger a blur of the most recently focused field and remove the - * autofill overlay if any set mutation observer is triggering - * excessive iterations. + * Checks if the inline menu button is visible at the top frame. */ - private isTriggeringExcessiveMutationObserverIterations() { - if (this.mutationObserverIterationsResetTimeout) { - clearTimeout(this.mutationObserverIterationsResetTimeout); - } + private async isInlineMenuButtonVisible() { + return (await this.sendExtensionMessage("checkIsAutofillInlineMenuButtonVisible")) === true; + } - this.mutationObserverIterations++; - this.mutationObserverIterationsResetTimeout = setTimeout( - () => (this.mutationObserverIterations = 0), - 2000, - ); + /** + * Checks if the inline menu list if visible at the top frame. + */ + private async isInlineMenuListVisible() { + return (await this.sendExtensionMessage("checkIsAutofillInlineMenuListVisible")) === true; + } - if (this.mutationObserverIterations > 100) { - clearTimeout(this.mutationObserverIterationsResetTimeout); - this.mutationObserverIterations = 0; - this.blurMostRecentOverlayField(); - this.removeAutofillOverlay(); + /** + * Checks if the field is currently focused within the top frame. + */ + private async isFieldCurrentlyFocused() { + return (await this.sendExtensionMessage("checkIsFieldCurrentlyFocused")) === true; + } - return true; - } - - return false; + /** + * Checks if the current tab contains ciphers that can be used to populate the inline menu. + */ + private async isInlineMenuCiphersPopulated() { + return (await this.sendExtensionMessage("checkIsInlineMenuCiphersPopulated")) === true; } /** @@ -1084,31 +1348,400 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte * @param element - The element to get the root node active element for. */ private getRootNodeActiveElement(element: Element): Element { + if (!element) { + return null; + } + const documentRoot = element.getRootNode() as ShadowRoot | Document; return documentRoot?.activeElement; } + /** + * Queries all iframe elements within the document and returns the + * sub frame offsets for each iframe element. + * + * @param message - The message object from the extension. + */ + private async getSubFrameOffsets( + message: AutofillExtensionMessage, + ): Promise { + const { subFrameUrl } = message; + + const subFrameUrlVariations = this.getSubFrameUrlVariations(subFrameUrl); + if (!subFrameUrlVariations) { + return null; + } + + let iframeElement: HTMLIFrameElement | null = null; + const iframeElements = globalThis.document.getElementsByTagName("iframe"); + + for (let iframeIndex = 0; iframeIndex < iframeElements.length; iframeIndex++) { + const iframe = iframeElements[iframeIndex]; + if (!subFrameUrlVariations.has(iframe.src)) { + continue; + } + + if (iframeElement) { + return null; + } + + iframeElement = iframe; + } + + if (!iframeElement) { + return null; + } + + return this.calculateSubFrameOffsets(iframeElement, subFrameUrl); + } + + /** + * Returns a set of all possible URL variations for the sub frame URL. + * + * @param subFrameUrl - The URL of the sub frame. + */ + private getSubFrameUrlVariations(subFrameUrl: string) { + try { + const url = new URL(subFrameUrl, globalThis.location.href); + const pathAndHash = url.pathname + url.hash; + const pathAndSearch = url.pathname + url.search; + const pathSearchAndHash = pathAndSearch + url.hash; + const pathNameWithoutTrailingSlash = url.pathname.replace(/\/$/, ""); + const pathWithoutTrailingSlashAndHash = pathNameWithoutTrailingSlash + url.hash; + const pathWithoutTrailingSlashAndSearch = pathNameWithoutTrailingSlash + url.search; + const pathWithoutTrailingSlashSearchAndHash = pathWithoutTrailingSlashAndSearch + url.hash; + + return new Set([ + url.href, + url.href.replace(/\/$/, ""), + url.pathname, + pathAndHash, + pathAndSearch, + pathSearchAndHash, + pathNameWithoutTrailingSlash, + pathWithoutTrailingSlashAndHash, + pathWithoutTrailingSlashAndSearch, + pathWithoutTrailingSlashSearchAndHash, + url.hostname + url.pathname, + url.hostname + pathAndHash, + url.hostname + pathAndSearch, + url.hostname + pathSearchAndHash, + url.hostname + pathNameWithoutTrailingSlash, + url.hostname + pathWithoutTrailingSlashAndHash, + url.hostname + pathWithoutTrailingSlashAndSearch, + url.hostname + pathWithoutTrailingSlashSearchAndHash, + url.origin + url.pathname, + url.origin + pathAndHash, + url.origin + pathAndSearch, + url.origin + pathSearchAndHash, + url.origin + pathNameWithoutTrailingSlash, + url.origin + pathWithoutTrailingSlashAndHash, + url.origin + pathWithoutTrailingSlashAndSearch, + url.origin + pathWithoutTrailingSlashSearchAndHash, + ]); + } catch (_error) { + return null; + } + } + + /** + * Posts a message to the parent frame to calculate the sub frame offset of the current frame. + * + * @param message - The message object from the extension. + */ + private getSubFrameOffsetsFromWindowMessage(message: any) { + globalThis.parent.postMessage( + { + command: "calculateSubFramePositioning", + subFrameData: { + url: window.location.href, + frameId: message.subFrameId, + left: 0, + top: 0, + parentFrameIds: [0], + subFrameDepth: 0, + } as SubFrameDataFromWindowMessage, + }, + "*", + ); + } + + /** + * Calculates the bounding rect for the queried frame and returns the + * offset data for the sub frame. + * + * @param iframeElement - The iframe element to calculate the sub frame offsets for. + * @param subFrameUrl - The URL of the sub frame. + * @param frameId - The frame ID of the sub frame. + */ + private calculateSubFrameOffsets( + iframeElement: HTMLIFrameElement, + subFrameUrl?: string, + frameId?: number, + ): SubFrameOffsetData { + const iframeRect = iframeElement.getBoundingClientRect(); + const iframeStyles = globalThis.getComputedStyle(iframeElement); + const paddingLeft = parseInt(iframeStyles.getPropertyValue("padding-left")) || 0; + const paddingTop = parseInt(iframeStyles.getPropertyValue("padding-top")) || 0; + const borderWidthLeft = parseInt(iframeStyles.getPropertyValue("border-left-width")) || 0; + const borderWidthTop = parseInt(iframeStyles.getPropertyValue("border-top-width")) || 0; + + return { + url: subFrameUrl, + frameId, + top: iframeRect.top + paddingTop + borderWidthTop, + left: iframeRect.left + paddingLeft + borderWidthLeft, + }; + } + + /** + * Calculates the sub frame positioning for the current frame + * through all parent frames until the top frame is reached. + * + * @param event - The message event. + */ + private calculateSubFramePositioning = async (event: MessageEvent) => { + const subFrameData: SubFrameDataFromWindowMessage = event.data.subFrameData; + + subFrameData.subFrameDepth++; + if (subFrameData.subFrameDepth >= MAX_SUB_FRAME_DEPTH) { + void this.sendExtensionMessage("destroyAutofillInlineMenuListeners", { subFrameData }); + return; + } + + let subFrameOffsets: SubFrameOffsetData; + const iframes = globalThis.document.querySelectorAll("iframe"); + for (let i = 0; i < iframes.length; i++) { + if (iframes[i].contentWindow === event.source) { + const iframeElement = iframes[i]; + subFrameOffsets = this.calculateSubFrameOffsets( + iframeElement, + subFrameData.url, + subFrameData.frameId, + ); + + subFrameData.top += subFrameOffsets.top; + subFrameData.left += subFrameOffsets.left; + + const parentFrameId = await this.sendExtensionMessage("getCurrentTabFrameId"); + if (typeof parentFrameId !== "undefined") { + subFrameData.parentFrameIds.push(parentFrameId); + } + + break; + } + } + + if (globalThis.window.self !== globalThis.window.top) { + globalThis.parent.postMessage({ command: "calculateSubFramePositioning", subFrameData }, "*"); + return; + } + + void this.sendExtensionMessage("updateSubFrameData", { subFrameData }); + }; + + /** + * Sets up global event listeners and the mutation + * observer to facilitate required changes to the + * overlay elements. + */ + private setupGlobalEventListeners = () => { + globalThis.addEventListener(EVENTS.MESSAGE, this.handleWindowMessageEvent); + globalThis.document.addEventListener(EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent); + globalThis.addEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent); + this.setOverlayRepositionEventListeners(); + }; + + /** + * Handles window messages that are sent to the current frame. Will trigger a + * calculation of the sub frame offsets through the parent frame. + * + * @param event - The message event. + */ + private handleWindowMessageEvent = (event: MessageEvent) => { + if (event.data?.command === "calculateSubFramePositioning") { + void this.calculateSubFramePositioning(event); + } + }; + + /** + * Handles the visibility change event. This method will remove the + * autofill overlay if the document is not visible. + */ + private handleVisibilityChangeEvent = () => { + if (!this.mostRecentlyFocusedField || globalThis.document.visibilityState === "visible") { + return; + } + + this.unsetMostRecentlyFocusedField(); + void this.sendExtensionMessage("closeAutofillInlineMenu", { + forceCloseInlineMenu: true, + }); + }; + + /** + * Sets up event listeners that facilitate repositioning + * the overlay elements on scroll or resize. + */ + private setOverlayRepositionEventListeners() { + const handler = this.useEventHandlersMemo( + throttle(this.handleOverlayRepositionEvent, 250), + AUTOFILL_OVERLAY_HANDLE_REPOSITION, + ); + globalThis.addEventListener(EVENTS.SCROLL, handler, { + capture: true, + passive: true, + }); + globalThis.addEventListener(EVENTS.RESIZE, handler); + } + + /** + * Removes the listeners that facilitate repositioning + * the overlay elements on scroll or resize. + */ + private removeOverlayRepositionEventListeners() { + const handler = this.eventHandlersMemo[AUTOFILL_OVERLAY_HANDLE_REPOSITION]; + globalThis.removeEventListener(EVENTS.SCROLL, handler, { + capture: true, + }); + globalThis.removeEventListener(EVENTS.RESIZE, handler); + + delete this.eventHandlersMemo[AUTOFILL_OVERLAY_HANDLE_REPOSITION]; + } + + /** + * Handles the resize or scroll events that enact + * repositioning of existing overlay elements. + */ + private handleOverlayRepositionEvent = async () => { + await this.sendExtensionMessage("triggerAutofillOverlayReposition"); + }; + + /** + * Sets up listeners that facilitate a rebuild of the sub frame offsets + * when a user interacts or focuses an element within the frame. + */ + private setupRebuildSubFrameOffsetsListeners = () => { + if (globalThis.window.top === globalThis.window || this.formFieldElements.size < 1) { + return; + } + this.removeSubFrameFocusOutListeners(); + + globalThis.addEventListener(EVENTS.FOCUS, this.handleSubFrameFocusInEvent); + globalThis.document.body.addEventListener(EVENTS.MOUSEENTER, this.handleSubFrameFocusInEvent); + }; + + /** + * Removes the listeners that facilitate a rebuild of the sub frame offsets. + */ + private removeRebuildSubFrameOffsetsListeners = () => { + globalThis.removeEventListener(EVENTS.FOCUS, this.handleSubFrameFocusInEvent); + globalThis.document.body.removeEventListener( + EVENTS.MOUSEENTER, + this.handleSubFrameFocusInEvent, + ); + }; + + /** + * Re-establishes listeners that handle the sub frame offsets rebuild of the frame + * based on user interaction with the sub frame. + */ + private setupSubFrameFocusOutListeners = () => { + globalThis.addEventListener(EVENTS.BLUR, this.setupRebuildSubFrameOffsetsListeners); + globalThis.document.body.addEventListener( + EVENTS.MOUSELEAVE, + this.setupRebuildSubFrameOffsetsListeners, + ); + }; + + /** + * Removes the listeners that trigger when a user focuses away from the sub frame. + */ + private removeSubFrameFocusOutListeners = () => { + globalThis.removeEventListener(EVENTS.BLUR, this.setupRebuildSubFrameOffsetsListeners); + globalThis.document.body.removeEventListener( + EVENTS.MOUSELEAVE, + this.setupRebuildSubFrameOffsetsListeners, + ); + }; + + /** + * Sends a message to the background script to trigger a rebuild of the sub frame + * offsets. Will deregister the listeners to ensure that other focus and mouse + * events do not unnecessarily re-trigger a sub frame rebuild. + */ + private handleSubFrameFocusInEvent = () => { + void this.sendExtensionMessage("triggerSubFrameFocusInRebuild"); + + this.removeRebuildSubFrameOffsetsListeners(); + this.setupSubFrameFocusOutListeners(); + }; + + /** + * Triggers an update in the most recently focused field's data and returns + * whether the field is within the viewport bounds. If not within the bounds + * of the viewport, the inline menu will be closed. + */ + private async checkIsMostRecentlyFocusedFieldWithinViewport() { + await this.updateMostRecentlyFocusedField(this.mostRecentlyFocusedField); + + const focusedFieldRectsTop = this.focusedFieldData?.focusedFieldRects?.top; + const focusedFieldRectsBottom = + focusedFieldRectsTop + this.focusedFieldData?.focusedFieldRects?.height; + const viewportHeight = globalThis.innerHeight + globalThis.scrollY; + return ( + !globalThis.isNaN(focusedFieldRectsTop) && + focusedFieldRectsTop >= 0 && + focusedFieldRectsTop < viewportHeight && + focusedFieldRectsBottom <= viewportHeight + ); + } + + /** + * Clears the timeout that triggers a debounced focus of the inline menu list. + */ + private clearFocusInlineMenuListTimeout() { + if (this.focusInlineMenuListTimeout) { + globalThis.clearTimeout(this.focusInlineMenuListTimeout); + } + } + + /** + * Clears the timeout that triggers the closing of the inline menu on a focus redirection. + */ + private clearCloseInlineMenuOnRedirectTimeout() { + if (this.closeInlineMenuOnRedirectTimeout) { + globalThis.clearTimeout(this.closeInlineMenuOnRedirectTimeout); + } + } + /** * Destroys the autofill overlay content service. This method will * disconnect the mutation observers and remove all event listeners. */ destroy() { - this.documentElementMutationObserver?.disconnect(); - this.clearUserInteractionEventTimeout(); - this.formFieldElements.forEach((formFieldElement) => { + this.clearFocusInlineMenuListTimeout(); + this.clearCloseInlineMenuOnRedirectTimeout(); + this.formFieldElements.forEach((_autofillField, formFieldElement) => { this.removeCachedFormFieldEventListeners(formFieldElement); formFieldElement.removeEventListener(EVENTS.BLUR, this.handleFormFieldBlurEvent); formFieldElement.removeEventListener(EVENTS.KEYUP, this.handleFormFieldKeyupEvent); this.formFieldElements.delete(formFieldElement); }); + Object.keys(this.userFilledFields).forEach((key) => { + if (this.userFilledFields[key]) { + delete this.userFilledFields[key]; + } + }); + this.userFilledFields = null; + globalThis.removeEventListener(EVENTS.MESSAGE, this.handleWindowMessageEvent); globalThis.document.removeEventListener( EVENTS.VISIBILITYCHANGE, this.handleVisibilityChangeEvent, ); globalThis.removeEventListener(EVENTS.FOCUSOUT, this.handleFormFieldBlurEvent); - this.removeAutofillOverlay(); this.removeOverlayRepositionEventListeners(); + this.removeRebuildSubFrameOffsetsListeners(); + this.removeSubFrameFocusOutListeners(); } } - -export default AutofillOverlayContentService; diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index dc9f3fcdbd4..455c171e59a 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -5,15 +5,18 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; -import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; +import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DefaultDomainSettingsService, DomainSettingsService, } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; +import { FeatureFlag, FeatureFlagValueType } from "@bitwarden/common/enums/feature-flag.enum"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { MessageListener } from "@bitwarden/common/platform/messaging"; @@ -26,6 +29,7 @@ import { subscribeTo, } from "@bitwarden/common/spec"; import { UserId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FieldType, LinkedIdType, LoginLinkedId, CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CardView } from "@bitwarden/common/vault/models/view/card.view"; @@ -34,13 +38,12 @@ import { FieldView } from "@bitwarden/common/vault/models/view/field.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"; -import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { BrowserApi } from "../../platform/browser/browser-api"; import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; import { AutofillMessageCommand, AutofillMessageSender } from "../enums/autofill-message.enums"; -import { AutofillPort } from "../enums/autofill-port.enums"; +import { AutofillPort } from "../enums/autofill-port.enum"; import AutofillField from "../models/autofill-field"; import AutofillPageDetails from "../models/autofill-page-details"; import AutofillScript from "../models/autofill-script"; @@ -59,7 +62,7 @@ import { GenerateFillScriptOptions, PageDetail, } from "./abstractions/autofill.service"; -import { AutoFillConstants, IdentityAutoFillConstants } from "./autofill-constants"; +import { AutoFillConstants } from "./autofill-constants"; import AutofillService from "./autofill.service"; const mockEquivalentDomains = [ @@ -72,7 +75,7 @@ describe("AutofillService", () => { let autofillService: AutofillService; const cipherService = mock(); let inlineMenuVisibilityMock$!: BehaviorSubject; - let autofillSettingsService: MockProxy; + let autofillSettingsService: MockProxy; const mockUserId = Utils.newGuid() as UserId; const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService); @@ -86,17 +89,27 @@ describe("AutofillService", () => { const platformUtilsService = mock(); let activeAccountStatusMock$: BehaviorSubject; let authService: MockProxy; + let configService: MockProxy; + let enableChangedPasswordPromptMock$: BehaviorSubject; + let enableAddedLoginPromptMock$: BehaviorSubject; + let userNotificationsSettings: MockProxy; let messageListener: MockProxy; beforeEach(() => { scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService); inlineMenuVisibilityMock$ = new BehaviorSubject(AutofillOverlayVisibility.OnFieldFocus); - autofillSettingsService = mock(); - (autofillSettingsService as any).inlineMenuVisibility$ = inlineMenuVisibilityMock$; + autofillSettingsService = mock(); + autofillSettingsService.inlineMenuVisibility$ = inlineMenuVisibilityMock$; activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Unlocked); authService = mock(); authService.activeAccountStatus$ = activeAccountStatusMock$; + configService = mock(); messageListener = mock(); + enableChangedPasswordPromptMock$ = new BehaviorSubject(true); + enableAddedLoginPromptMock$ = new BehaviorSubject(true); + userNotificationsSettings = mock(); + userNotificationsSettings.enableChangedPasswordPrompt$ = enableChangedPasswordPromptMock$; + userNotificationsSettings.enableAddedLoginPrompt$ = enableAddedLoginPromptMock$; autofillService = new AutofillService( cipherService, autofillSettingsService, @@ -109,6 +122,8 @@ describe("AutofillService", () => { scriptInjectorService, accountService, authService, + configService, + userNotificationsSettings, messageListener, ); domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); @@ -213,7 +228,7 @@ describe("AutofillService", () => { .spyOn(BrowserApi, "getAllFrameDetails") .mockResolvedValue([mock({ frameId: 0 })]); jest - .spyOn(autofillService, "getOverlayVisibility") + .spyOn(autofillService, "getInlineMenuVisibility") .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true); }); @@ -275,13 +290,13 @@ describe("AutofillService", () => { expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( tab1, - "updateAutofillOverlayVisibility", - { autofillOverlayVisibility: AutofillOverlayVisibility.OnButtonClick }, + "updateAutofillInlineMenuVisibility", + { inlineMenuVisibility: AutofillOverlayVisibility.OnButtonClick }, ); expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( tab2, - "updateAutofillOverlayVisibility", - { autofillOverlayVisibility: AutofillOverlayVisibility.OnButtonClick }, + "updateAutofillInlineMenuVisibility", + { inlineMenuVisibility: AutofillOverlayVisibility.OnButtonClick }, ); }); @@ -292,13 +307,13 @@ describe("AutofillService", () => { expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( tab1, - "updateAutofillOverlayVisibility", - { autofillOverlayVisibility: AutofillOverlayVisibility.OnFieldFocus }, + "updateAutofillInlineMenuVisibility", + { inlineMenuVisibility: AutofillOverlayVisibility.OnFieldFocus }, ); expect(BrowserApi.tabSendMessageData).toHaveBeenCalledWith( tab2, - "updateAutofillOverlayVisibility", - { autofillOverlayVisibility: AutofillOverlayVisibility.OnFieldFocus }, + "updateAutofillInlineMenuVisibility", + { inlineMenuVisibility: AutofillOverlayVisibility.OnFieldFocus }, ); }); }); @@ -345,25 +360,38 @@ describe("AutofillService", () => { describe("injectAutofillScripts", () => { const autofillBootstrapScript = "bootstrap-autofill.js"; const autofillOverlayBootstrapScript = "bootstrap-autofill-overlay.js"; + const autofillOverlayMenuBootstrapScript = "bootstrap-autofill-overlay-menu.js"; + const autofillOverlayNotificationsBootstrapScript = + "bootstrap-autofill-overlay-notifications.js"; const defaultAutofillScripts = ["autofiller.js", "notificationBar.js", "contextMenuHandler.js"]; const defaultExecuteScriptOptions = { runAt: "document_start" }; let tabMock: chrome.tabs.Tab; let sender: chrome.runtime.MessageSender; beforeEach(() => { + configService.getFeatureFlag.mockImplementation( + async (_feature) => true as FeatureFlagValueType, + ); tabMock = createChromeTabMock(); sender = { tab: tabMock, frameId: 1 }; jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation(); jest - .spyOn(autofillService, "getOverlayVisibility") + .spyOn(autofillService, "getInlineMenuVisibility") .mockResolvedValue(AutofillOverlayVisibility.OnFieldFocus); jest.spyOn(autofillService, "getAutofillOnPageLoad").mockResolvedValue(true); }); it("accepts an extension message sender and injects the autofill scripts into the tab of the sender", async () => { + configService.getFeatureFlag.mockImplementation(async (_feature) => { + if (_feature === FeatureFlag.NotificationBarAddLoginImprovements) { + return false as FeatureFlagValueType; + } + + return true as FeatureFlagValueType; + }); await autofillService.injectAutofillScripts(sender.tab, sender.frameId, true); - [autofillOverlayBootstrapScript, ...defaultAutofillScripts].forEach((scriptName) => { + [autofillOverlayMenuBootstrapScript, ...defaultAutofillScripts].forEach((scriptName) => { expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, { file: `content/${scriptName}`, frameId: sender.frameId, @@ -413,8 +441,15 @@ describe("AutofillService", () => { it("will inject the bootstrap-autofill script if the user does not have the autofill overlay enabled", async () => { jest - .spyOn(autofillService, "getOverlayVisibility") + .spyOn(autofillService, "getInlineMenuVisibility") .mockResolvedValue(AutofillOverlayVisibility.Off); + configService.getFeatureFlag.mockImplementation(async (_feature) => { + if (_feature === FeatureFlag.NotificationBarAddLoginImprovements) { + return false as FeatureFlagValueType; + } + + return true as FeatureFlagValueType; + }); await autofillService.injectAutofillScripts(sender.tab, sender.frameId); @@ -430,6 +465,21 @@ describe("AutofillService", () => { }); }); + it("will inject the bootstrap-autofill-overlay-notifications script if the user has the notification bar turned on but does not have the inline menu turned on", async () => { + jest + .spyOn(autofillService, "getInlineMenuVisibility") + .mockResolvedValue(AutofillOverlayVisibility.Off); + enableChangedPasswordPromptMock$.next(true); + + await autofillService.injectAutofillScripts(sender.tab, sender.frameId); + + expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, { + file: `content/${autofillOverlayNotificationsBootstrapScript}`, + frameId: sender.frameId, + ...defaultExecuteScriptOptions, + }); + }); + it("injects the content-message-handler script if not injecting on page load", async () => { await autofillService.injectAutofillScripts(sender.tab, sender.frameId, false); @@ -575,8 +625,8 @@ describe("AutofillService", () => { describe("doAutoFill", () => { let autofillOptions: AutoFillOptions; - const nothingToAutofillError = "Nothing to auto-fill."; - const didNotAutofillError = "Did not auto-fill."; + const nothingToAutofillError = "Nothing to autofill."; + const didNotAutofillError = "Did not autofill."; beforeEach(() => { autofillOptions = { @@ -691,6 +741,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, @@ -704,7 +755,6 @@ describe("AutofillService", () => { { command: "fillForm", fillScript: { - autosubmit: null, metadata: {}, properties: { delay_between_operations: 20, @@ -800,7 +850,7 @@ describe("AutofillService", () => { triggerTestFailure(); } catch (error) { expect(logService.info).toHaveBeenCalledWith( - "Auto-fill on page load was blocked due to an untrusted iframe.", + "Autofill on page load was blocked due to an untrusted iframe.", ); expect(error.message).toBe(didNotAutofillError); } @@ -814,7 +864,7 @@ describe("AutofillService", () => { await autofillService.doAutoFill(autofillOptions); expect(logService.info).not.toHaveBeenCalledWith( - "Auto-fill on page load was blocked due to an untrusted iframe.", + "Autofill on page load was blocked due to an untrusted iframe.", ); }); @@ -1010,6 +1060,7 @@ describe("AutofillService", () => { fillNewPassword: fromCommand, allowUntrustedIframe: fromCommand, allowTotpAutofill: fromCommand, + autoSubmitLogin: false, }); expect(result).toBe(totpCode); }); @@ -1039,6 +1090,7 @@ describe("AutofillService", () => { fillNewPassword: fromCommand, allowUntrustedIframe: fromCommand, allowTotpAutofill: fromCommand, + autoSubmitLogin: false, }); expect(result).toBe(totpCode); }); @@ -1065,6 +1117,7 @@ describe("AutofillService", () => { fillNewPassword: fromCommand, allowUntrustedIframe: fromCommand, allowTotpAutofill: fromCommand, + autoSubmitLogin: false, }); expect(result).toBe(totpCode); }); @@ -1199,7 +1252,7 @@ describe("AutofillService", () => { expect(result).toBe(totp); }); - it("auto-fills card cipher types", async () => { + it("autofills card cipher types", async () => { const cardFormPageDetails = [ { frameId: 1, @@ -1227,27 +1280,26 @@ describe("AutofillService", () => { jest.spyOn(autofillService as any, "getActiveTab").mockResolvedValueOnce(tab); jest.spyOn(autofillService, "doAutoFill").mockImplementation(); jest - .spyOn(autofillService["cipherService"], "getAllDecryptedForUrl") - .mockResolvedValueOnce([cardCipher]); + .spyOn(autofillService["cipherService"], "getNextCardCipher") + .mockResolvedValueOnce(cardCipher); - await autofillService.doAutoFillActiveTab(cardFormPageDetails, false, CipherType.Card); + await autofillService.doAutoFillActiveTab(cardFormPageDetails, true, CipherType.Card); - expect(autofillService["cipherService"].getAllDecryptedForUrl).toHaveBeenCalled(); expect(autofillService.doAutoFill).toHaveBeenCalledWith({ tab: tab, cipher: cardCipher, pageDetails: cardFormPageDetails, - skipLastUsed: true, - skipUsernameOnlyFill: true, - onlyEmptyFields: true, - onlyVisibleFields: true, + skipLastUsed: false, + skipUsernameOnlyFill: false, + onlyEmptyFields: false, + onlyVisibleFields: false, fillNewPassword: false, - allowUntrustedIframe: false, + allowUntrustedIframe: true, allowTotpAutofill: false, }); }); - it("auto-fills identity cipher types", async () => { + it("autofills identity cipher types", async () => { const identityFormPageDetails = [ { frameId: 1, @@ -1275,26 +1327,21 @@ describe("AutofillService", () => { jest.spyOn(autofillService as any, "getActiveTab").mockResolvedValueOnce(tab); jest.spyOn(autofillService, "doAutoFill").mockImplementation(); jest - .spyOn(autofillService["cipherService"], "getAllDecryptedForUrl") - .mockResolvedValueOnce([identityCipher]); + .spyOn(autofillService["cipherService"], "getNextIdentityCipher") + .mockResolvedValueOnce(identityCipher); - await autofillService.doAutoFillActiveTab( - identityFormPageDetails, - false, - CipherType.Identity, - ); + await autofillService.doAutoFillActiveTab(identityFormPageDetails, true, CipherType.Identity); - expect(autofillService["cipherService"].getAllDecryptedForUrl).toHaveBeenCalled(); expect(autofillService.doAutoFill).toHaveBeenCalledWith({ tab: tab, cipher: identityCipher, pageDetails: identityFormPageDetails, - skipLastUsed: true, - skipUsernameOnlyFill: true, - onlyEmptyFields: true, - onlyVisibleFields: true, + skipLastUsed: false, + skipUsernameOnlyFill: false, + onlyEmptyFields: false, + onlyVisibleFields: false, fillNewPassword: false, - allowUntrustedIframe: false, + allowUntrustedIframe: true, allowTotpAutofill: false, }); }); @@ -1549,7 +1596,6 @@ describe("AutofillService", () => { expect(autofillService["generateLoginFillScript"]).toHaveBeenCalledWith( { - autosubmit: null, metadata: {}, properties: {}, script: [ @@ -1588,7 +1634,6 @@ describe("AutofillService", () => { expect(autofillService["generateCardFillScript"]).toHaveBeenCalledWith( { - autosubmit: null, metadata: {}, properties: {}, script: [ @@ -1627,7 +1672,6 @@ describe("AutofillService", () => { expect(autofillService["generateIdentityFillScript"]).toHaveBeenCalledWith( { - autosubmit: null, metadata: {}, properties: {}, script: [ @@ -2431,10 +2475,10 @@ describe("AutofillService", () => { options.cipher.card = mock(); }); - it("returns null if the passed options contains a cipher with no card view", () => { + it("returns null if the passed options contains a cipher with no card view", async () => { options.cipher.card = undefined; - const value = autofillService["generateCardFillScript"]( + const value = await autofillService["generateCardFillScript"]( fillScript, pageDetails, filledFields, @@ -2455,7 +2499,7 @@ describe("AutofillService", () => { untrustedIframe: false, }; - it("returns an unmodified fill script when the field is a `span` field", () => { + it("returns an unmodified fill script when the field is a `span` field", async () => { const spanField = createAutofillFieldMock({ opid: "span-field", form: "validFormId", @@ -2466,7 +2510,7 @@ describe("AutofillService", () => { pageDetails.fields = [spanField]; jest.spyOn(AutofillService, "isExcludedFieldType"); - const value = autofillService["generateCardFillScript"]( + const value = await autofillService["generateCardFillScript"]( fillScript, pageDetails, filledFields, @@ -2478,7 +2522,7 @@ describe("AutofillService", () => { }); AutoFillConstants.ExcludedAutofillTypes.forEach((excludedType) => { - it(`returns an unmodified fill script when the field has a '${excludedType}' type`, () => { + it(`returns an unmodified fill script when the field has a '${excludedType}' type`, async () => { const invalidField = createAutofillFieldMock({ opid: `${excludedType}-field`, form: "validFormId", @@ -2489,7 +2533,7 @@ describe("AutofillService", () => { pageDetails.fields = [invalidField]; jest.spyOn(AutofillService, "isExcludedFieldType"); - const value = autofillService["generateCardFillScript"]( + const value = await autofillService["generateCardFillScript"]( fillScript, pageDetails, filledFields, @@ -2504,7 +2548,7 @@ describe("AutofillService", () => { }); }); - it("returns an unmodified fill script when the field is not viewable", () => { + it("returns an unmodified fill script when the field is not viewable", async () => { const notViewableField = createAutofillFieldMock({ opid: "invalid-field", form: "validFormId", @@ -2517,7 +2561,7 @@ describe("AutofillService", () => { jest.spyOn(AutofillService, "forCustomFieldsOnly"); jest.spyOn(AutofillService, "isExcludedFieldType"); - const value = autofillService["generateCardFillScript"]( + const value = await autofillService["generateCardFillScript"]( fillScript, pageDetails, filledFields, @@ -2619,8 +2663,8 @@ describe("AutofillService", () => { jest.spyOn(autofillService as any, "makeScriptActionWithValue"); }); - it("returns a fill script containing all of the passed card fields", () => { - const value = autofillService["generateCardFillScript"]( + it("returns a fill script containing all of the passed card fields", async () => { + const value = await autofillService["generateCardFillScript"]( fillScript, pageDetails, filledFields, @@ -2701,11 +2745,11 @@ describe("AutofillService", () => { options.cipher.card.expMonth = "05"; }); - it("returns an expiration month parsed from found select options within the field", () => { + it("returns an expiration month parsed from found select options within the field", async () => { const testValue = "sometestvalue"; expMonthField.selectInfo.options[4] = ["May", testValue]; - const value = autofillService["generateCardFillScript"]( + const value = await autofillService["generateCardFillScript"]( fillScript, pageDetails, filledFields, @@ -2715,12 +2759,12 @@ describe("AutofillService", () => { expect(value.script[2]).toStrictEqual(["fill_by_opid", expMonthField.opid, testValue]); }); - it("returns an expiration month parsed from found select options within the field when the select field has an empty option at the end of the list of options", () => { + it("returns an expiration month parsed from found select options within the field when the select field has an empty option at the end of the list of options", async () => { const testValue = "sometestvalue"; expMonthField.selectInfo.options[4] = ["May", testValue]; expMonthField.selectInfo.options.push(["", ""]); - const value = autofillService["generateCardFillScript"]( + const value = await autofillService["generateCardFillScript"]( fillScript, pageDetails, filledFields, @@ -2730,12 +2774,12 @@ describe("AutofillService", () => { expect(value.script[2]).toStrictEqual(["fill_by_opid", expMonthField.opid, testValue]); }); - it("returns an expiration month parsed from found select options within the field when the select field has an empty option at the start of the list of options", () => { + it("returns an expiration month parsed from found select options within the field when the select field has an empty option at the start of the list of options", async () => { const testValue = "sometestvalue"; expMonthField.selectInfo.options[4] = ["May", testValue]; expMonthField.selectInfo.options.unshift(["", ""]); - const value = autofillService["generateCardFillScript"]( + const value = await autofillService["generateCardFillScript"]( fillScript, pageDetails, filledFields, @@ -2745,13 +2789,13 @@ describe("AutofillService", () => { expect(value.script[2]).toStrictEqual(["fill_by_opid", expMonthField.opid, testValue]); }); - it("returns an expiration month with a zero attached if the field requires two characters, and the vault item has only one character", () => { + it("returns an expiration month with a zero attached if the field requires two characters, and the vault item has only one character", async () => { options.cipher.card.expMonth = "5"; expMonthField.selectInfo = null; expMonthField.placeholder = "mm"; expMonthField.maxLength = 2; - const value = autofillService["generateCardFillScript"]( + const value = await autofillService["generateCardFillScript"]( fillScript, pageDetails, filledFields, @@ -2786,12 +2830,12 @@ describe("AutofillService", () => { options.cipher.card.expYear = "2024"; }); - it("returns an expiration year parsed from the select options if an exact match is found for either the select option text or value", () => { + it("returns an expiration year parsed from the select options if an exact match is found for either the select option text or value", async () => { const someTestValue = "sometestvalue"; expYearField.selectInfo.options[1] = ["2024", someTestValue]; options.cipher.card.expYear = someTestValue; - let value = autofillService["generateCardFillScript"]( + let value = await autofillService["generateCardFillScript"]( fillScript, pageDetails, filledFields, @@ -2802,7 +2846,7 @@ describe("AutofillService", () => { expYearField.selectInfo.options[1] = [someTestValue, "2024"]; - value = autofillService["generateCardFillScript"]( + value = await autofillService["generateCardFillScript"]( fillScript, pageDetails, filledFields, @@ -2812,12 +2856,12 @@ describe("AutofillService", () => { expect(value.script[2]).toStrictEqual(["fill_by_opid", expYearField.opid, someTestValue]); }); - it("returns an expiration year parsed from the select options if the value of an option contains only two characters and the vault item value contains four characters", () => { + it("returns an expiration year parsed from the select options if the value of an option contains only two characters and the vault item value contains four characters", async () => { const yearValue = "26"; expYearField.selectInfo.options.push(["The year 2026", yearValue]); options.cipher.card.expYear = "2026"; - const value = autofillService["generateCardFillScript"]( + const value = await autofillService["generateCardFillScript"]( fillScript, pageDetails, filledFields, @@ -2827,13 +2871,13 @@ describe("AutofillService", () => { expect(value.script[2]).toStrictEqual(["fill_by_opid", expYearField.opid, yearValue]); }); - it("returns an expiration year parsed from the select options if the vault of an option is separated by a colon", () => { + it("returns an expiration year parsed from the select options if the vault of an option is separated by a colon", async () => { const yearValue = "26"; const colonSeparatedYearValue = `2:0${yearValue}`; expYearField.selectInfo.options.push(["The year 2026", colonSeparatedYearValue]); options.cipher.card.expYear = yearValue; - const value = autofillService["generateCardFillScript"]( + const value = await autofillService["generateCardFillScript"]( fillScript, pageDetails, filledFields, @@ -2847,14 +2891,14 @@ describe("AutofillService", () => { ]); }); - it("returns an expiration year with `20` prepended to the vault item value if the field to be filled expects a `yyyy` format but the vault item only has two characters", () => { + it("returns an expiration year with `20` prepended to the vault item value if the field to be filled expects a `yyyy` format but the vault item only has two characters", async () => { const yearValue = "26"; expYearField.selectInfo = null; expYearField.placeholder = "yyyy"; expYearField.maxLength = 4; options.cipher.card.expYear = yearValue; - const value = autofillService["generateCardFillScript"]( + const value = await autofillService["generateCardFillScript"]( fillScript, pageDetails, filledFields, @@ -2868,14 +2912,14 @@ describe("AutofillService", () => { ]); }); - it("returns an expiration year with only the last two values if the field to be filled expects a `yy` format but the vault item contains four characters", () => { + it("returns an expiration year with only the last two values if the field to be filled expects a `yy` format but the vault item contains four characters", async () => { const yearValue = "26"; expYearField.selectInfo = null; expYearField.placeholder = "yy"; expYearField.maxLength = 2; options.cipher.card.expYear = `20${yearValue}`; - const value = autofillService["generateCardFillScript"]( + const value = await autofillService["generateCardFillScript"]( fillScript, pageDetails, filledFields, @@ -2886,11 +2930,26 @@ describe("AutofillService", () => { }); }); + const expectedDateFormats = [ + ["mm/yyyy", "05/2024"], + ["mm/yy", "05/24"], + ["yyyy/mm", "2024/05"], + ["yy/mm", "24/05"], + ["mm-yyyy", "05-2024"], + ["mm-yy", "05-24"], + ["yyyy-mm", "2024-05"], + ["yy-mm", "24-05"], + ["yyyymm", "202405"], + ["yymm", "2405"], + ["mmyyyy", "052024"], + ["mmyy", "0524"], + ]; describe("given a generic expiration date field", () => { let expirationDateField: AutofillField; let expirationDateFieldView: FieldView; beforeEach(() => { + configService.getFeatureFlag.mockResolvedValue(false); expirationDateField = createAutofillFieldMock({ opid: "expirationDate", form: "validFormId", @@ -2905,23 +2964,11 @@ describe("AutofillService", () => { options.cipher.card.expYear = "2024"; }); - const expectedDateFormats = [ - ["mm/yyyy", "05/2024"], - ["mm/yy", "05/24"], - ["yyyy/mm", "2024/05"], - ["yy/mm", "24/05"], - ["mm-yyyy", "05-2024"], - ["mm-yy", "05-24"], - ["yyyy-mm", "2024-05"], - ["yy-mm", "24-05"], - ["yyyymm", "202405"], - ["yymm", "2405"], - ["mmyyyy", "052024"], - ["mmyy", "0524"], - ]; expectedDateFormats.forEach((dateFormat, index) => { - it(`returns an expiration date format matching '${dateFormat[0]}'`, () => { + it(`returns an expiration date format matching '${dateFormat[0]}'`, async () => { expirationDateField.placeholder = dateFormat[0]; + + // test alternate stored cipher value formats if (index === 0) { options.cipher.card.expYear = "24"; } @@ -2929,7 +2976,13 @@ describe("AutofillService", () => { options.cipher.card.expMonth = "5"; } - const value = autofillService["generateCardFillScript"]( + const enableNewCardCombinedExpiryAutofill = await configService.getFeatureFlag( + FeatureFlag.EnableNewCardCombinedExpiryAutofill, + ); + + expect(enableNewCardCombinedExpiryAutofill).toEqual(false); + + const value = await autofillService["generateCardFillScript"]( fillScript, pageDetails, filledFields, @@ -2940,17 +2993,128 @@ describe("AutofillService", () => { }); }); - it("returns an expiration date format matching `yyyy-mm` if no valid format can be identified", () => { - const value = autofillService["generateCardFillScript"]( + it("returns an expiration date format matching `yyyy-mm` if no valid format can be identified", async () => { + const value = await autofillService["generateCardFillScript"]( fillScript, pageDetails, filledFields, options, ); + const enableNewCardCombinedExpiryAutofill = await configService.getFeatureFlag( + FeatureFlag.EnableNewCardCombinedExpiryAutofill, + ); + + expect(enableNewCardCombinedExpiryAutofill).toEqual(false); + expect(value.script[2]).toStrictEqual(["fill_by_opid", "expirationDate", "2024-05"]); }); }); + + const extraExpectedDateFormats = [ + ...expectedDateFormats, + ["m yy", "5 24"], + ["m yyyy", "5 2024"], + ["m-yy", "5-24"], + ["m-yyyy", "5-2024"], + ["m.yy", "5.24"], + ["m.yyyy", "5.2024"], + ["m/yy", "5/24"], + ["m/yyyy", "5/2024"], + ["mm åååå", "05 2024"], + ["mm yy", "05 24"], + ["mm yyyy", "05 2024"], + ["mm.yy", "05.24"], + ["mm.yyyy", "05.2024"], + ["myy", "524"], + ["myyyy", "52024"], + ["yy m", "24 5"], + ["yy mm", "24 05"], + ["yy mm", "24 05"], + ["yy-m", "24-5"], + ["yy.m", "24.5"], + ["yy.mm", "24.05"], + ["yy/m", "24/5"], + ["yym", "245"], + ["yyyy m", "2024 5"], + ["yyyy mm", "2024 05"], + ["yyyy-m", "2024-5"], + ["yyyy.m", "2024.5"], + ["yyyy.mm", "2024.05"], + ["yyyy/m", "2024/5"], + ["yyyym", "20245"], + ["мм гг", "05 24"], + ]; + describe("given a generic expiration date field with the `enable-new-card-combined-expiry-autofill` feature-flag enabled", () => { + let expirationDateField: AutofillField; + let expirationDateFieldView: FieldView; + + beforeEach(() => { + configService.getFeatureFlag.mockResolvedValue(true); + expirationDateField = createAutofillFieldMock({ + opid: "expirationDate", + form: "validFormId", + elementNumber: 3, + htmlName: "expiration-date", + }); + filledFields["exp-field"] = expirationDateField; + expirationDateFieldView = mock({ name: "exp" }); + pageDetails.fields = [expirationDateField]; + options.cipher.fields = [expirationDateFieldView]; + options.cipher.card.expMonth = "05"; + options.cipher.card.expYear = "2024"; + }); + + afterEach(() => { + configService.getFeatureFlag.mockResolvedValue(false); + }); + + extraExpectedDateFormats.forEach((dateFormat, index) => { + it(`feature-flagged logic returns an expiration date format matching '${dateFormat[0]}'`, async () => { + expirationDateField.placeholder = dateFormat[0]; + + // test alternate stored cipher value formats + if (index === 0) { + options.cipher.card.expYear = "24"; + } + if (index === 1) { + options.cipher.card.expMonth = "05"; + } + + const enableNewCardCombinedExpiryAutofill = await configService.getFeatureFlag( + FeatureFlag.EnableNewCardCombinedExpiryAutofill, + ); + + expect(enableNewCardCombinedExpiryAutofill).toEqual(true); + + const value = await autofillService["generateCardFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(value.script[2]).toStrictEqual(["fill_by_opid", "expirationDate", dateFormat[1]]); + }); + }); + + it("feature-flagged logic returns an expiration date format matching `mm/yy` if no valid format can be identified", async () => { + const value = await autofillService["generateCardFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + const enableNewCardCombinedExpiryAutofill = await configService.getFeatureFlag( + FeatureFlag.EnableNewCardCombinedExpiryAutofill, + ); + + expect(enableNewCardCombinedExpiryAutofill).toEqual(true); + + expect(value.script[2]).toStrictEqual(["fill_by_opid", "expirationDate", "05/24"]); + }); + }); }); describe("inUntrustedIframe", () => { @@ -3057,12 +3221,12 @@ describe("AutofillService", () => { options.cipher.identity = mock(); }); - it("returns null if an identify is not found within the cipher", () => { + it("returns null if an identify is not found within the cipher", async () => { options.cipher.identity = null; jest.spyOn(autofillService as any, "makeScriptAction"); jest.spyOn(autofillService as any, "makeScriptActionWithValue"); - const value = autofillService["generateIdentityFillScript"]( + const value = await autofillService["generateIdentityFillScript"]( fillScript, pageDetails, filledFields, @@ -3088,432 +3252,389 @@ describe("AutofillService", () => { jest.spyOn(autofillService as any, "makeScriptActionWithValue"); }); - it("will not attempt to match custom fields", () => { - const customField = createAutofillFieldMock({ tagName: "span" }); - pageDetails.fields.push(customField); + let isRefactorFeatureFlagSet = false; + for (let index = 0; index < 2; index++) { + describe(`when the isRefactorFeatureFlagSet is ${isRefactorFeatureFlagSet}`, () => { + beforeEach(() => { + configService.getFeatureFlag.mockResolvedValue(isRefactorFeatureFlagSet); + }); - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); + afterAll(() => { + isRefactorFeatureFlagSet = true; + }); - expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(customField); - expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled(); - expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled(); - expect(value.script).toStrictEqual([]); - }); + it("will not attempt to match custom fields", async () => { + const customField = createAutofillFieldMock({ tagName: "span" }); + pageDetails.fields.push(customField); - it("will not attempt to match a field that is of an excluded type", () => { - const excludedField = createAutofillFieldMock({ type: "hidden" }); - pageDetails.fields.push(excludedField); + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); + expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(customField); + expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled(); + expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled(); + expect(value.script).toStrictEqual([]); + }); - expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(excludedField); - expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalledWith( - excludedField, - AutoFillConstants.ExcludedAutofillTypes, - ); - expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled(); - expect(value.script).toStrictEqual([]); - }); + it("will not attempt to match a field that is of an excluded type", async () => { + const excludedField = createAutofillFieldMock({ type: "hidden" }); + pageDetails.fields.push(excludedField); - it("will not attempt to match a field that is not viewable", () => { - const viewableField = createAutofillFieldMock({ viewable: false }); - pageDetails.fields.push(viewableField); + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); + expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(excludedField); + expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalledWith( + excludedField, + AutoFillConstants.ExcludedAutofillTypes, + ); + expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled(); + expect(value.script).toStrictEqual([]); + }); - expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(viewableField); - expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled(); - expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled(); - expect(value.script).toStrictEqual([]); - }); + it("will not attempt to match a field that is not viewable", async () => { + const viewableField = createAutofillFieldMock({ viewable: false }); + pageDetails.fields.push(viewableField); - it("will match a full name field to the vault item identity value", () => { - const fullNameField = createAutofillFieldMock({ opid: "fullName", htmlName: "full-name" }); - pageDetails.fields = [fullNameField]; - options.cipher.identity.firstName = firstName; - options.cipher.identity.middleName = middleName; - options.cipher.identity.lastName = lastName; + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); + expect(AutofillService.forCustomFieldsOnly).toHaveBeenCalledWith(viewableField); + expect(AutofillService["isExcludedFieldType"]).toHaveBeenCalled(); + expect(AutofillService["isFieldMatch"]).not.toHaveBeenCalled(); + expect(value.script).toStrictEqual([]); + }); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - fullNameField.htmlName, - IdentityAutoFillConstants.FullNameFieldNames, - IdentityAutoFillConstants.FullNameFieldNameValues, - ); - expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( - fillScript, - `${firstName} ${middleName} ${lastName}`, - fullNameField, - filledFields, - ); - expect(value.script[2]).toStrictEqual([ - "fill_by_opid", - fullNameField.opid, - `${firstName} ${middleName} ${lastName}`, - ]); - }); + it("will match a full name field to the vault item identity value", async () => { + const fullNameField = createAutofillFieldMock({ + opid: "fullName", + htmlName: "full-name", + }); + pageDetails.fields = [fullNameField]; + options.cipher.identity.firstName = firstName; + options.cipher.identity.middleName = middleName; + options.cipher.identity.lastName = lastName; - it("will match a full name field to the a vault item that only has a last name", () => { - const fullNameField = createAutofillFieldMock({ opid: "fullName", htmlName: "full-name" }); - pageDetails.fields = [fullNameField]; - options.cipher.identity.firstName = ""; - options.cipher.identity.middleName = ""; - options.cipher.identity.lastName = lastName; + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); + expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( + fillScript, + `${firstName} ${middleName} ${lastName}`, + fullNameField, + filledFields, + ); + expect(value.script[2]).toStrictEqual([ + "fill_by_opid", + fullNameField.opid, + `${firstName} ${middleName} ${lastName}`, + ]); + }); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - fullNameField.htmlName, - IdentityAutoFillConstants.FullNameFieldNames, - IdentityAutoFillConstants.FullNameFieldNameValues, - ); - expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( - fillScript, - lastName, - fullNameField, - filledFields, - ); - expect(value.script[2]).toStrictEqual(["fill_by_opid", fullNameField.opid, lastName]); - }); + it("will match a full name field to the a vault item that only has a last name", async () => { + const fullNameField = createAutofillFieldMock({ + opid: "fullName", + htmlName: "full-name", + }); + pageDetails.fields = [fullNameField]; + options.cipher.identity.firstName = ""; + options.cipher.identity.middleName = ""; + options.cipher.identity.lastName = lastName; - it("will match first name, middle name, and last name fields to the vault item identity value", () => { - const firstNameField = createAutofillFieldMock({ - opid: "firstName", - htmlName: "first-name", + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( + fillScript, + lastName, + fullNameField, + filledFields, + ); + expect(value.script[2]).toStrictEqual(["fill_by_opid", fullNameField.opid, lastName]); + }); + + it("will match first name, middle name, and last name fields to the vault item identity value", async () => { + const firstNameField = createAutofillFieldMock({ + opid: "firstName", + htmlName: "first-name", + }); + const middleNameField = createAutofillFieldMock({ + opid: "middleName", + htmlName: "middle-name", + }); + const lastNameField = createAutofillFieldMock({ + opid: "lastName", + htmlName: "last-name", + }); + pageDetails.fields = [firstNameField, middleNameField, lastNameField]; + options.cipher.identity.firstName = firstName; + options.cipher.identity.middleName = middleName; + options.cipher.identity.lastName = lastName; + + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( + fillScript, + options.cipher.identity.firstName, + firstNameField, + filledFields, + ); + expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( + fillScript, + options.cipher.identity.middleName, + middleNameField, + filledFields, + ); + expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( + fillScript, + options.cipher.identity.lastName, + lastNameField, + filledFields, + ); + expect(value.script[2]).toStrictEqual(["fill_by_opid", firstNameField.opid, firstName]); + expect(value.script[5]).toStrictEqual([ + "fill_by_opid", + middleNameField.opid, + middleName, + ]); + expect(value.script[8]).toStrictEqual(["fill_by_opid", lastNameField.opid, lastName]); + }); + + it("will match title and email fields to the vault item identity value", async () => { + const titleField = createAutofillFieldMock({ opid: "title", htmlName: "title" }); + const emailField = createAutofillFieldMock({ opid: "email", htmlName: "email" }); + pageDetails.fields = [titleField, emailField]; + const title = "Mr."; + const email = "email@example.com"; + options.cipher.identity.title = title; + options.cipher.identity.email = email; + + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( + fillScript, + options.cipher.identity.title, + titleField, + filledFields, + ); + expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( + fillScript, + options.cipher.identity.email, + emailField, + filledFields, + ); + expect(value.script[2]).toStrictEqual(["fill_by_opid", titleField.opid, title]); + expect(value.script[5]).toStrictEqual(["fill_by_opid", emailField.opid, email]); + }); + + it("will match a full address field to the vault item identity values", async () => { + const fullAddressField = createAutofillFieldMock({ + opid: "fullAddress", + htmlName: "address", + }); + pageDetails.fields = [fullAddressField]; + const address1 = "123 Main St."; + const address2 = "Apt. 1"; + const address3 = "P.O. Box 123"; + options.cipher.identity.address1 = address1; + options.cipher.identity.address2 = address2; + options.cipher.identity.address3 = address3; + + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( + fillScript, + `${address1}, ${address2}, ${address3}`, + fullAddressField, + filledFields, + ); + expect(value.script[2]).toStrictEqual([ + "fill_by_opid", + fullAddressField.opid, + `${address1}, ${address2}, ${address3}`, + ]); + }); + + it("will match address1, address2, address3, postalCode, city, state, country, phone, username, and company fields to their corresponding vault item identity values", async () => { + const address1Field = createAutofillFieldMock({ + opid: "address1", + htmlName: "address-1", + }); + const address2Field = createAutofillFieldMock({ + opid: "address2", + htmlName: "address-2", + }); + const address3Field = createAutofillFieldMock({ + opid: "address3", + htmlName: "address-3", + }); + const postalCodeField = createAutofillFieldMock({ + opid: "postalCode", + htmlName: "postal-code", + }); + const cityField = createAutofillFieldMock({ opid: "city", htmlName: "city" }); + const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" }); + const countryField = createAutofillFieldMock({ opid: "country", htmlName: "country" }); + const phoneField = createAutofillFieldMock({ opid: "phone", htmlName: "phone" }); + const usernameField = createAutofillFieldMock({ + opid: "username", + htmlName: "username", + }); + const companyField = createAutofillFieldMock({ opid: "company", htmlName: "company" }); + pageDetails.fields = [ + address1Field, + address2Field, + address3Field, + postalCodeField, + cityField, + stateField, + countryField, + phoneField, + usernameField, + companyField, + ]; + const address1 = "123 Main St."; + const address2 = "Apt. 1"; + const address3 = "P.O. Box 123"; + const postalCode = "12345"; + const city = "City"; + const state = "TX"; + const country = "US"; + const phone = "123-456-7890"; + const username = "username"; + const company = "Company"; + options.cipher.identity.address1 = address1; + options.cipher.identity.address2 = address2; + options.cipher.identity.address3 = address3; + options.cipher.identity.postalCode = postalCode; + options.cipher.identity.city = city; + options.cipher.identity.state = state; + options.cipher.identity.country = country; + options.cipher.identity.phone = phone; + options.cipher.identity.username = username; + options.cipher.identity.company = company; + + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(value.script).toContainEqual(["fill_by_opid", address1Field.opid, address1]); + expect(value.script).toContainEqual(["fill_by_opid", address2Field.opid, address2]); + expect(value.script).toContainEqual(["fill_by_opid", address3Field.opid, address3]); + expect(value.script).toContainEqual(["fill_by_opid", postalCodeField.opid, postalCode]); + expect(value.script).toContainEqual(["fill_by_opid", cityField.opid, city]); + expect(value.script).toContainEqual(["fill_by_opid", stateField.opid, state]); + expect(value.script).toContainEqual(["fill_by_opid", countryField.opid, country]); + expect(value.script).toContainEqual(["fill_by_opid", phoneField.opid, phone]); + expect(value.script).toContainEqual(["fill_by_opid", usernameField.opid, username]); + expect(value.script).toContainEqual(["fill_by_opid", companyField.opid, company]); + }); + + it("will find the two character IsoState value for an identity cipher that contains the full name of a state", async () => { + const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" }); + pageDetails.fields = [stateField]; + const state = "California"; + options.cipher.identity.state = state; + + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( + fillScript, + "CA", + expect.anything(), + expect.anything(), + ); + expect(value.script[2]).toStrictEqual(["fill_by_opid", stateField.opid, "CA"]); + }); + + it("will find the two character IsoProvince value for an identity cipher that contains the full name of a province", async () => { + const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" }); + pageDetails.fields = [stateField]; + const state = "Ontario"; + options.cipher.identity.state = state; + + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( + fillScript, + "ON", + expect.anything(), + expect.anything(), + ); + expect(value.script[2]).toStrictEqual(["fill_by_opid", stateField.opid, "ON"]); + }); + + it("will find the two character IsoCountry value for an identity cipher that contains the full name of a country", async () => { + const countryField = createAutofillFieldMock({ opid: "country", htmlName: "country" }); + pageDetails.fields = [countryField]; + const country = "Somalia"; + options.cipher.identity.country = country; + + const value = await autofillService["generateIdentityFillScript"]( + fillScript, + pageDetails, + filledFields, + options, + ); + + expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( + fillScript, + "SO", + expect.anything(), + expect.anything(), + ); + expect(value.script[2]).toStrictEqual(["fill_by_opid", countryField.opid, "SO"]); + }); }); - const middleNameField = createAutofillFieldMock({ - opid: "middleName", - htmlName: "middle-name", - }); - const lastNameField = createAutofillFieldMock({ opid: "lastName", htmlName: "last-name" }); - pageDetails.fields = [firstNameField, middleNameField, lastNameField]; - options.cipher.identity.firstName = firstName; - options.cipher.identity.middleName = middleName; - options.cipher.identity.lastName = lastName; - - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); - - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - firstNameField.htmlName, - IdentityAutoFillConstants.FirstnameFieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - middleNameField.htmlName, - IdentityAutoFillConstants.MiddlenameFieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - lastNameField.htmlName, - IdentityAutoFillConstants.LastnameFieldNames, - ); - expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith( - fillScript, - options.cipher.identity, - expect.anything(), - filledFields, - firstNameField.opid, - ); - expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith( - fillScript, - options.cipher.identity, - expect.anything(), - filledFields, - middleNameField.opid, - ); - expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith( - fillScript, - options.cipher.identity, - expect.anything(), - filledFields, - lastNameField.opid, - ); - expect(value.script[2]).toStrictEqual(["fill_by_opid", firstNameField.opid, firstName]); - expect(value.script[5]).toStrictEqual(["fill_by_opid", middleNameField.opid, middleName]); - expect(value.script[8]).toStrictEqual(["fill_by_opid", lastNameField.opid, lastName]); - }); - - it("will match title and email fields to the vault item identity value", () => { - const titleField = createAutofillFieldMock({ opid: "title", htmlName: "title" }); - const emailField = createAutofillFieldMock({ opid: "email", htmlName: "email" }); - pageDetails.fields = [titleField, emailField]; - const title = "Mr."; - const email = "email@example.com"; - options.cipher.identity.title = title; - options.cipher.identity.email = email; - - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); - - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - titleField.htmlName, - IdentityAutoFillConstants.TitleFieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - emailField.htmlName, - IdentityAutoFillConstants.EmailFieldNames, - ); - expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith( - fillScript, - options.cipher.identity, - expect.anything(), - filledFields, - titleField.opid, - ); - expect(autofillService["makeScriptAction"]).toHaveBeenCalledWith( - fillScript, - options.cipher.identity, - expect.anything(), - filledFields, - emailField.opid, - ); - expect(value.script[2]).toStrictEqual(["fill_by_opid", titleField.opid, title]); - expect(value.script[5]).toStrictEqual(["fill_by_opid", emailField.opid, email]); - }); - - it("will match a full address field to the vault item identity values", () => { - const fullAddressField = createAutofillFieldMock({ - opid: "fullAddress", - htmlName: "address", - }); - pageDetails.fields = [fullAddressField]; - const address1 = "123 Main St."; - const address2 = "Apt. 1"; - const address3 = "P.O. Box 123"; - options.cipher.identity.address1 = address1; - options.cipher.identity.address2 = address2; - options.cipher.identity.address3 = address3; - - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); - - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - fullAddressField.htmlName, - IdentityAutoFillConstants.AddressFieldNames, - IdentityAutoFillConstants.AddressFieldNameValues, - ); - expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( - fillScript, - `${address1}, ${address2}, ${address3}`, - fullAddressField, - filledFields, - ); - expect(value.script[2]).toStrictEqual([ - "fill_by_opid", - fullAddressField.opid, - `${address1}, ${address2}, ${address3}`, - ]); - }); - - it("will match address1, address2, address3, postalCode, city, state, country, phone, username, and company fields to their corresponding vault item identity values", () => { - const address1Field = createAutofillFieldMock({ opid: "address1", htmlName: "address-1" }); - const address2Field = createAutofillFieldMock({ opid: "address2", htmlName: "address-2" }); - const address3Field = createAutofillFieldMock({ opid: "address3", htmlName: "address-3" }); - const postalCodeField = createAutofillFieldMock({ - opid: "postalCode", - htmlName: "postal-code", - }); - const cityField = createAutofillFieldMock({ opid: "city", htmlName: "city" }); - const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" }); - const countryField = createAutofillFieldMock({ opid: "country", htmlName: "country" }); - const phoneField = createAutofillFieldMock({ opid: "phone", htmlName: "phone" }); - const usernameField = createAutofillFieldMock({ opid: "username", htmlName: "username" }); - const companyField = createAutofillFieldMock({ opid: "company", htmlName: "company" }); - pageDetails.fields = [ - address1Field, - address2Field, - address3Field, - postalCodeField, - cityField, - stateField, - countryField, - phoneField, - usernameField, - companyField, - ]; - const address1 = "123 Main St."; - const address2 = "Apt. 1"; - const address3 = "P.O. Box 123"; - const postalCode = "12345"; - const city = "City"; - const state = "State"; - const country = "Country"; - const phone = "123-456-7890"; - const username = "username"; - const company = "Company"; - options.cipher.identity.address1 = address1; - options.cipher.identity.address2 = address2; - options.cipher.identity.address3 = address3; - options.cipher.identity.postalCode = postalCode; - options.cipher.identity.city = city; - options.cipher.identity.state = state; - options.cipher.identity.country = country; - options.cipher.identity.phone = phone; - options.cipher.identity.username = username; - options.cipher.identity.company = company; - - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); - - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - address1Field.htmlName, - IdentityAutoFillConstants.Address1FieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - address2Field.htmlName, - IdentityAutoFillConstants.Address2FieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - address3Field.htmlName, - IdentityAutoFillConstants.Address3FieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - postalCodeField.htmlName, - IdentityAutoFillConstants.PostalCodeFieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - cityField.htmlName, - IdentityAutoFillConstants.CityFieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - stateField.htmlName, - IdentityAutoFillConstants.StateFieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - countryField.htmlName, - IdentityAutoFillConstants.CountryFieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - phoneField.htmlName, - IdentityAutoFillConstants.PhoneFieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - usernameField.htmlName, - IdentityAutoFillConstants.UserNameFieldNames, - ); - expect(AutofillService["isFieldMatch"]).toHaveBeenCalledWith( - companyField.htmlName, - IdentityAutoFillConstants.CompanyFieldNames, - ); - expect(autofillService["makeScriptAction"]).toHaveBeenCalled(); - expect(value.script[2]).toStrictEqual(["fill_by_opid", address1Field.opid, address1]); - expect(value.script[5]).toStrictEqual(["fill_by_opid", address2Field.opid, address2]); - expect(value.script[8]).toStrictEqual(["fill_by_opid", address3Field.opid, address3]); - expect(value.script[11]).toStrictEqual(["fill_by_opid", cityField.opid, city]); - expect(value.script[14]).toStrictEqual(["fill_by_opid", postalCodeField.opid, postalCode]); - expect(value.script[17]).toStrictEqual(["fill_by_opid", companyField.opid, company]); - expect(value.script[20]).toStrictEqual(["fill_by_opid", phoneField.opid, phone]); - expect(value.script[23]).toStrictEqual(["fill_by_opid", usernameField.opid, username]); - expect(value.script[26]).toStrictEqual(["fill_by_opid", stateField.opid, state]); - expect(value.script[29]).toStrictEqual(["fill_by_opid", countryField.opid, country]); - }); - - it("will find the two character IsoState value for an identity cipher that contains the full name of a state", () => { - const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" }); - pageDetails.fields = [stateField]; - const state = "California"; - options.cipher.identity.state = state; - - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); - - expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( - fillScript, - "CA", - expect.anything(), - expect.anything(), - ); - expect(value.script[2]).toStrictEqual(["fill_by_opid", stateField.opid, "CA"]); - }); - - it("will find the two character IsoProvince value for an identity cipher that contains the full name of a province", () => { - const stateField = createAutofillFieldMock({ opid: "state", htmlName: "state" }); - pageDetails.fields = [stateField]; - const state = "Ontario"; - options.cipher.identity.state = state; - - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); - - expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( - fillScript, - "ON", - expect.anything(), - expect.anything(), - ); - expect(value.script[2]).toStrictEqual(["fill_by_opid", stateField.opid, "ON"]); - }); - - it("will find the two character IsoCountry value for an identity cipher that contains the full name of a country", () => { - const countryField = createAutofillFieldMock({ opid: "country", htmlName: "country" }); - pageDetails.fields = [countryField]; - const country = "Somalia"; - options.cipher.identity.country = country; - - const value = autofillService["generateIdentityFillScript"]( - fillScript, - pageDetails, - filledFields, - options, - ); - - expect(autofillService["makeScriptActionWithValue"]).toHaveBeenCalledWith( - fillScript, - "SO", - expect.anything(), - expect.anything(), - ); - expect(value.script[2]).toStrictEqual(["fill_by_opid", countryField.opid, "SO"]); - }); + } }); }); diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 4c37cd1f07f..49d00624f34 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -2,34 +2,41 @@ import { filter, firstValueFrom, Observable, scan, startWith } from "rxjs"; import { pairwise } from "rxjs/operators"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { UriMatchStrategySetting, UriMatchStrategy, } from "@bitwarden/common/models/domain/domain-service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessageListener } from "@bitwarden/common/platform/messaging"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { FieldType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; +import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; +import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"; +import { normalizeExpiryYearFormat } from "@bitwarden/common/vault/utils"; import { BrowserApi } from "../../platform/browser/browser-api"; import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; import { openVaultItemPasswordRepromptPopout } from "../../vault/popup/utils/vault-popout-window"; import { AutofillMessageCommand, AutofillMessageSender } from "../enums/autofill-message.enums"; -import { AutofillPort } from "../enums/autofill-port.enums"; +import { AutofillPort } from "../enums/autofill-port.enum"; import AutofillField from "../models/autofill-field"; import AutofillPageDetails from "../models/autofill-page-details"; import AutofillScript from "../models/autofill-script"; @@ -44,6 +51,7 @@ import { } from "./abstractions/autofill.service"; import { AutoFillConstants, + CardExpiryDateFormat, CreditCardAutoFillConstants, IdentityAutoFillConstants, } from "./autofill-constants"; @@ -67,6 +75,8 @@ export default class AutofillService implements AutofillServiceInterface { private scriptInjectorService: ScriptInjectorService, private accountService: AccountService, private authService: AuthService, + private configService: ConfigService, + private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, private messageListener: MessageListener, ) {} @@ -160,18 +170,14 @@ export default class AutofillService implements AutofillServiceInterface { const activeAccount = await firstValueFrom(this.accountService.activeAccount$); const authStatus = await firstValueFrom(this.authService.activeAccountStatus$); const accountIsUnlocked = authStatus === AuthenticationStatus.Unlocked; - let overlayVisibility: InlineMenuVisibilitySetting = AutofillOverlayVisibility.Off; let autoFillOnPageLoadIsEnabled = false; + const addLoginImprovementsFlagActive = await this.configService.getFeatureFlag( + FeatureFlag.NotificationBarAddLoginImprovements, + ); - if (activeAccount) { - overlayVisibility = await this.getOverlayVisibility(); - } - - const mainAutofillScript = overlayVisibility - ? "bootstrap-autofill-overlay.js" - : "bootstrap-autofill.js"; - - const injectedScripts = [mainAutofillScript]; + const injectedScripts = [ + await this.getBootstrapAutofillContentScript(activeAccount, addLoginImprovementsFlagActive), + ]; if (activeAccount && accountIsUnlocked) { autoFillOnPageLoadIsEnabled = await this.getAutofillOnPageLoad(); @@ -188,7 +194,11 @@ export default class AutofillService implements AutofillServiceInterface { }); } - injectedScripts.push("notificationBar.js", "contextMenuHandler.js"); + if (!addLoginImprovementsFlagActive) { + injectedScripts.push("notificationBar.js"); + } + + injectedScripts.push("contextMenuHandler.js"); for (const injectedScript of injectedScripts) { await this.scriptInjectorService.inject({ @@ -202,6 +212,55 @@ export default class AutofillService implements AutofillServiceInterface { } } + /** + * Identifies the correct autofill script to inject based on whether the + * inline menu is enabled, and whether the user has the notification bar + * enabled. + * + * @param activeAccount - The active account + * @param addLoginImprovementsFlagActive - Whether the add login improvements feature flag is active + */ + private async getBootstrapAutofillContentScript( + activeAccount: { id: UserId | undefined } & AccountInfo, + addLoginImprovementsFlagActive = false, + ): Promise { + let inlineMenuVisibility: InlineMenuVisibilitySetting = AutofillOverlayVisibility.Off; + + if (activeAccount) { + inlineMenuVisibility = await this.getInlineMenuVisibility(); + } + + const inlineMenuPositioningImprovements = await this.configService.getFeatureFlag( + FeatureFlag.InlineMenuPositioningImprovements, + ); + if (!inlineMenuPositioningImprovements) { + return "bootstrap-legacy-autofill-overlay.js"; + } + + const enableChangedPasswordPrompt = await firstValueFrom( + this.userNotificationSettingsService.enableChangedPasswordPrompt$, + ); + const enableAddedLoginPrompt = await firstValueFrom( + this.userNotificationSettingsService.enableAddedLoginPrompt$, + ); + const isNotificationBarEnabled = + addLoginImprovementsFlagActive && (enableChangedPasswordPrompt || enableAddedLoginPrompt); + + if (!inlineMenuVisibility && !isNotificationBarEnabled) { + return "bootstrap-autofill.js"; + } + + if (!inlineMenuVisibility && isNotificationBarEnabled) { + return "bootstrap-autofill-overlay-notifications.js"; + } + + if (inlineMenuVisibility && !isNotificationBarEnabled) { + return "bootstrap-autofill-overlay-menu.js"; + } + + return "bootstrap-autofill-overlay.js"; + } + /** * Gets all forms with password fields and formats the data * for both forms and password input elements. @@ -274,7 +333,7 @@ export default class AutofillService implements AutofillServiceInterface { /** * Gets the overlay's visibility setting from the autofill settings service. */ - async getOverlayVisibility(): Promise { + async getInlineMenuVisibility(): Promise { return await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$); } @@ -307,7 +366,7 @@ export default class AutofillService implements AutofillServiceInterface { async doAutoFill(options: AutoFillOptions): Promise { const tab = options.tab; if (!tab || !options.cipher || !options.pageDetails || !options.pageDetails.length) { - throw new Error("Nothing to auto-fill."); + throw new Error("Nothing to autofill."); } let totp: string | null = null; @@ -335,6 +394,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, @@ -349,7 +409,7 @@ export default class AutofillService implements AutofillServiceInterface { options.allowUntrustedIframe != undefined && !options.allowUntrustedIframe ) { - this.logService.info("Auto-fill on page load was blocked due to an untrusted iframe."); + this.logService.info("Autofill on page load was blocked due to an untrusted iframe."); return; } @@ -368,7 +428,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, @@ -404,7 +464,7 @@ export default class AutofillService implements AutofillServiceInterface { return null; } } else { - throw new Error("Did not auto-fill."); + throw new Error("Did not autofill."); } } @@ -413,12 +473,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) { @@ -458,6 +520,7 @@ export default class AutofillService implements AutofillServiceInterface { fillNewPassword: fromCommand, allowUntrustedIframe: fromCommand, allowTotpAutofill: fromCommand, + autoSubmitLogin, }); // Update last used index as autofill has succeeded @@ -468,6 +531,12 @@ export default class AutofillService implements AutofillServiceInterface { return totpCode; } + /** + * Checks if the cipher requires password reprompt and opens the password reprompt popout if necessary. + * + * @param cipher - The cipher to autofill + * @param tab - The tab to autofill + */ async isPasswordRepromptRequired(cipher: CipherView, tab: chrome.tabs.Tab): Promise { const userHasMasterPasswordAndKeyHash = await this.userVerificationService.hasMasterPasswordAndMasterKeyHash(); @@ -510,16 +579,30 @@ export default class AutofillService implements AutofillServiceInterface { return await this.doAutoFillOnTab(pageDetails, tab, fromCommand); } - // Cipher is a non-login type - const cipher: CipherView = ( - (await this.cipherService.getAllDecryptedForUrl(tab.url, [cipherType])) || [] - ).find(({ type }) => type === cipherType); + let cipher: CipherView; + let cacheKey = ""; - if (!cipher || cipher.reprompt !== CipherRepromptType.None) { + if (cipherType === CipherType.Card) { + cacheKey = "cardCiphers"; + cipher = await this.cipherService.getNextCardCipher(); + } else { + cacheKey = "identityCiphers"; + cipher = await this.cipherService.getNextIdentityCipher(); + } + + if (!cipher || !cacheKey || (cipher.reprompt === CipherRepromptType.Password && !fromCommand)) { return null; } - return await this.doAutoFill({ + if (await this.isPasswordRepromptRequired(cipher, tab)) { + if (fromCommand) { + this.cipherService.updateLastUsedIndexForUrl(cacheKey); + } + + return null; + } + + const totpCode = await this.doAutoFill({ tab: tab, cipher: cipher, pageDetails: pageDetails, @@ -531,6 +614,25 @@ export default class AutofillService implements AutofillServiceInterface { allowUntrustedIframe: fromCommand, allowTotpAutofill: false, }); + + if (fromCommand) { + this.cipherService.updateLastUsedIndexForUrl(cacheKey); + } + + 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); + } } /** @@ -621,10 +723,15 @@ export default class AutofillService implements AutofillServiceInterface { ); break; case CipherType.Card: - fillScript = this.generateCardFillScript(fillScript, pageDetails, filledFields, options); + fillScript = await this.generateCardFillScript( + fillScript, + pageDetails, + filledFields, + options, + ); break; case CipherType.Identity: - fillScript = this.generateIdentityFillScript( + fillScript = await this.generateIdentityFillScript( fillScript, pageDetails, filledFields, @@ -783,6 +890,7 @@ export default class AutofillService implements AutofillServiceInterface { }); } + const formElementsSet = new Set(); usernames.forEach((u) => { // eslint-disable-next-line if (filledFields.hasOwnProperty(u.opid)) { @@ -791,6 +899,7 @@ export default class AutofillService implements AutofillServiceInterface { filledFields[u.opid] = u; AutofillService.fillByOpid(fillScript, u, login.username); + formElementsSet.add(u.form); }); passwords.forEach((p) => { @@ -801,8 +910,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) => { @@ -830,12 +944,12 @@ export default class AutofillService implements AutofillServiceInterface { * @returns {AutofillScript|null} * @private */ - private generateCardFillScript( + private async generateCardFillScript( fillScript: AutofillScript, pageDetails: AutofillPageDetails, filledFields: { [id: string]: AutofillField }, options: GenerateFillScriptOptions, - ): AutofillScript | null { + ): Promise { if (!options.cipher.card) { return null; } @@ -920,6 +1034,7 @@ export default class AutofillService implements AutofillServiceInterface { this.makeScriptAction(fillScript, card, fillFields, filledFields, "code"); this.makeScriptAction(fillScript, card, fillFields, filledFields, "brand"); + // There is an expiration month field and the cipher has an expiration month value if (fillFields.expMonth && AutofillService.hasValue(card.expMonth)) { let expMonth: string = card.expMonth; @@ -958,6 +1073,7 @@ export default class AutofillService implements AutofillServiceInterface { AutofillService.fillByOpid(fillScript, fillFields.expMonth, expMonth); } + // There is an expiration year field and the cipher has an expiration year value if (fillFields.expYear && AutofillService.hasValue(card.expYear)) { let expYear: string = card.expYear; if (fillFields.expYear.selectInfo && fillFields.expYear.selectInfo.options) { @@ -989,7 +1105,7 @@ export default class AutofillService implements AutofillServiceInterface { fillFields.expYear.maxLength === 4 ) { if (expYear.length === 2) { - expYear = "20" + expYear; + expYear = normalizeExpiryYearFormat(expYear); } } else if ( this.fieldAttrsContain(fillFields.expYear, "yy") || @@ -1004,142 +1120,174 @@ export default class AutofillService implements AutofillServiceInterface { AutofillService.fillByOpid(fillScript, fillFields.expYear, expYear); } + // There is a single expiry date field (combined values) and the cipher has both expiration month and year if ( fillFields.exp && AutofillService.hasValue(card.expMonth) && AutofillService.hasValue(card.expYear) ) { - const fullMonth = ("0" + card.expMonth).slice(-2); + let combinedExpiryFillValue = null; - let fullYear: string = card.expYear; - let partYear: string = null; - if (fullYear.length === 2) { - partYear = fullYear; - fullYear = "20" + fullYear; - } else if (fullYear.length === 4) { - partYear = fullYear.substr(2, 2); - } + const enableNewCardCombinedExpiryAutofill = await this.configService.getFeatureFlag( + FeatureFlag.EnableNewCardCombinedExpiryAutofill, + ); - let exp: string = null; - for (let i = 0; i < CreditCardAutoFillConstants.MonthAbbr.length; i++) { - if ( - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.MonthAbbr[i] + - "/" + - CreditCardAutoFillConstants.YearAbbrLong[i], - ) - ) { - exp = fullMonth + "/" + fullYear; - } else if ( - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.MonthAbbr[i] + - "/" + - CreditCardAutoFillConstants.YearAbbrShort[i], - ) && - partYear != null - ) { - exp = fullMonth + "/" + partYear; - } else if ( - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.YearAbbrLong[i] + - "/" + - CreditCardAutoFillConstants.MonthAbbr[i], - ) - ) { - exp = fullYear + "/" + fullMonth; - } else if ( - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.YearAbbrShort[i] + - "/" + - CreditCardAutoFillConstants.MonthAbbr[i], - ) && - partYear != null - ) { - exp = partYear + "/" + fullMonth; - } else if ( - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.MonthAbbr[i] + - "-" + - CreditCardAutoFillConstants.YearAbbrLong[i], - ) - ) { - exp = fullMonth + "-" + fullYear; - } else if ( - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.MonthAbbr[i] + - "-" + - CreditCardAutoFillConstants.YearAbbrShort[i], - ) && - partYear != null - ) { - exp = fullMonth + "-" + partYear; - } else if ( - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.YearAbbrLong[i] + - "-" + - CreditCardAutoFillConstants.MonthAbbr[i], - ) - ) { - exp = fullYear + "-" + fullMonth; - } else if ( - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.YearAbbrShort[i] + - "-" + - CreditCardAutoFillConstants.MonthAbbr[i], - ) && - partYear != null - ) { - exp = partYear + "-" + fullMonth; - } else if ( - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.YearAbbrLong[i] + CreditCardAutoFillConstants.MonthAbbr[i], - ) - ) { - exp = fullYear + fullMonth; - } else if ( - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.YearAbbrShort[i] + CreditCardAutoFillConstants.MonthAbbr[i], - ) && - partYear != null - ) { - exp = partYear + fullMonth; - } else if ( - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.MonthAbbr[i] + CreditCardAutoFillConstants.YearAbbrLong[i], - ) - ) { - exp = fullMonth + fullYear; - } else if ( - this.fieldAttrsContain( - fillFields.exp, - CreditCardAutoFillConstants.MonthAbbr[i] + CreditCardAutoFillConstants.YearAbbrShort[i], - ) && - partYear != null - ) { - exp = fullMonth + partYear; + if (enableNewCardCombinedExpiryAutofill) { + combinedExpiryFillValue = this.generateCombinedExpiryValue(card, fillFields.exp); + } else { + const fullMonth = ("0" + card.expMonth).slice(-2); + + let fullYear: string = card.expYear; + let partYear: string = null; + if (fullYear.length === 2) { + partYear = fullYear; + fullYear = normalizeExpiryYearFormat(fullYear); + } else if (fullYear.length === 4) { + partYear = fullYear.substr(2, 2); } - if (exp != null) { - break; + for (let i = 0; i < CreditCardAutoFillConstants.MonthAbbr.length; i++) { + if ( + // mm/yyyy + this.fieldAttrsContain( + fillFields.exp, + CreditCardAutoFillConstants.MonthAbbr[i] + + "/" + + CreditCardAutoFillConstants.YearAbbrLong[i], + ) + ) { + combinedExpiryFillValue = fullMonth + "/" + fullYear; + } else if ( + // mm/yy + this.fieldAttrsContain( + fillFields.exp, + CreditCardAutoFillConstants.MonthAbbr[i] + + "/" + + CreditCardAutoFillConstants.YearAbbrShort[i], + ) && + partYear != null + ) { + combinedExpiryFillValue = fullMonth + "/" + partYear; + } else if ( + // yyyy/mm + this.fieldAttrsContain( + fillFields.exp, + CreditCardAutoFillConstants.YearAbbrLong[i] + + "/" + + CreditCardAutoFillConstants.MonthAbbr[i], + ) + ) { + combinedExpiryFillValue = fullYear + "/" + fullMonth; + } else if ( + // yy/mm + this.fieldAttrsContain( + fillFields.exp, + CreditCardAutoFillConstants.YearAbbrShort[i] + + "/" + + CreditCardAutoFillConstants.MonthAbbr[i], + ) && + partYear != null + ) { + combinedExpiryFillValue = partYear + "/" + fullMonth; + } else if ( + // mm-yyyy + this.fieldAttrsContain( + fillFields.exp, + CreditCardAutoFillConstants.MonthAbbr[i] + + "-" + + CreditCardAutoFillConstants.YearAbbrLong[i], + ) + ) { + combinedExpiryFillValue = fullMonth + "-" + fullYear; + } else if ( + // mm-yy + this.fieldAttrsContain( + fillFields.exp, + CreditCardAutoFillConstants.MonthAbbr[i] + + "-" + + CreditCardAutoFillConstants.YearAbbrShort[i], + ) && + partYear != null + ) { + combinedExpiryFillValue = fullMonth + "-" + partYear; + } else if ( + // yyyy-mm + this.fieldAttrsContain( + fillFields.exp, + CreditCardAutoFillConstants.YearAbbrLong[i] + + "-" + + CreditCardAutoFillConstants.MonthAbbr[i], + ) + ) { + combinedExpiryFillValue = fullYear + "-" + fullMonth; + } else if ( + // yy-mm + this.fieldAttrsContain( + fillFields.exp, + CreditCardAutoFillConstants.YearAbbrShort[i] + + "-" + + CreditCardAutoFillConstants.MonthAbbr[i], + ) && + partYear != null + ) { + combinedExpiryFillValue = partYear + "-" + fullMonth; + } else if ( + // yyyymm + this.fieldAttrsContain( + fillFields.exp, + CreditCardAutoFillConstants.YearAbbrLong[i] + + CreditCardAutoFillConstants.MonthAbbr[i], + ) + ) { + combinedExpiryFillValue = fullYear + fullMonth; + } else if ( + // yymm + this.fieldAttrsContain( + fillFields.exp, + CreditCardAutoFillConstants.YearAbbrShort[i] + + CreditCardAutoFillConstants.MonthAbbr[i], + ) && + partYear != null + ) { + combinedExpiryFillValue = partYear + fullMonth; + } else if ( + // mmyyyy + this.fieldAttrsContain( + fillFields.exp, + CreditCardAutoFillConstants.MonthAbbr[i] + + CreditCardAutoFillConstants.YearAbbrLong[i], + ) + ) { + combinedExpiryFillValue = fullMonth + fullYear; + } else if ( + // mmyy + this.fieldAttrsContain( + fillFields.exp, + CreditCardAutoFillConstants.MonthAbbr[i] + + CreditCardAutoFillConstants.YearAbbrShort[i], + ) && + partYear != null + ) { + combinedExpiryFillValue = fullMonth + partYear; + } + + if (combinedExpiryFillValue != null) { + break; + } + } + + // If none of the previous cases applied, set as default + if (combinedExpiryFillValue == null) { + combinedExpiryFillValue = fullYear + "-" + fullMonth; } } - if (exp == null) { - exp = fullYear + "-" + fullMonth; - } - - this.makeScriptActionWithValue(fillScript, exp, fillFields.exp, filledFields); + this.makeScriptActionWithValue( + fillScript, + combinedExpiryFillValue, + fillFields.exp, + filledFields, + ); } return fillScript; @@ -1157,7 +1305,7 @@ export default class AutofillService implements AutofillServiceInterface { options: GenerateFillScriptOptions, ): Promise { // If the pageUrl (from the content script) matches the tabUrl (from the sender tab), we are not in an iframe - // This also avoids a false positive if no URI is saved and the user triggers auto-fill anyway + // This also avoids a false positive if no URI is saved and the user triggers autofill anyway if (pageUrl === options.tabUrl) { return false; } @@ -1180,28 +1328,169 @@ export default class AutofillService implements AutofillServiceInterface { * Used when handling autofill on credit card fields. Determines whether * the field has an attribute that matches the given value. * @param {AutofillField} field - * @param {string} containsVal + * @param {string} containsValue * @returns {boolean} * @private */ - private fieldAttrsContain(field: AutofillField, containsVal: string): boolean { + private fieldAttrsContain(field: AutofillField, containsValue: string): boolean { if (!field) { return false; } - let doesContain = false; - CreditCardAutoFillConstants.CardAttributesExtended.forEach((attr) => { - // eslint-disable-next-line - if (doesContain || !field.hasOwnProperty(attr) || !field[attr]) { + let doesContainValue = false; + CreditCardAutoFillConstants.CardAttributesExtended.forEach((attributeName) => { + // eslint-disable-next-line no-prototype-builtins + if (doesContainValue || !field[attributeName]) { return; } - let val = field[attr]; - val = val.replace(/ /g, "").toLowerCase(); - doesContain = val.indexOf(containsVal) > -1; + let fieldValue = field[attributeName]; + fieldValue = fieldValue.replace(/ /g, "").toLowerCase(); + doesContainValue = fieldValue.indexOf(containsValue) > -1; }); - return doesContain; + return doesContainValue; + } + + /** + * Returns a string value representation of the combined card expiration month and year values + * in a format matching discovered guidance within the field attributes (typically provided for users). + * + * @param {CardView} cardCipher + * @param {AutofillField} field + */ + private generateCombinedExpiryValue(cardCipher: CardView, field: AutofillField): string { + /* + Some expectations of the passed stored card cipher view: + + - At the time of writing, the stored card expiry year value (`expYear`) + can be any arbitrary string (no format validation). We may attempt some format + normalization here, but expect the user to have entered a string of integers + with a length of 2 or 4 + + - the `expiration` property cannot be used for autofill as it is an opinionated + format + + - `expMonth` a stringified integer stored with no zero-padding and is not + zero-indexed (e.g. January is "1", not "01" or 0) + */ + + // Expiry format options + let useMonthPadding = true; + let useYearFull = false; + let delimiter = "/"; + let orderByYear = false; + + // Because users are allowed to store truncated years, we need to make assumptions + // about the full year format when called for + const currentCentury = `${new Date().getFullYear()}`.slice(0, 2); + + // Note, we construct the output rather than doing string replacement against the + // format guidance pattern to avoid edge cases that would output invalid values + const [ + // The guidance parsed from the field properties regarding expiry format + expectedExpiryDateFormat, + // The (localized) date pattern set that was used to parse the expiry format guidance + expiryDateFormatPatterns, + ] = this.getExpectedExpiryDateFormat(field); + + if (expectedExpiryDateFormat) { + const { Month, MonthShort, Year } = expiryDateFormatPatterns; + + const expiryDateDelimitersPattern = + "\\" + CreditCardAutoFillConstants.CardExpiryDateDelimiters.join("\\"); + + // assign the delimiter from the expected format string + delimiter = + expectedExpiryDateFormat.match(new RegExp(`[${expiryDateDelimitersPattern}]`, "g"))?.[0] || + ""; + + // check if the expected format starts with a month form + // order matters here; check long form first, since short form will match against long + if (expectedExpiryDateFormat.indexOf(Month + delimiter) === 0) { + useMonthPadding = true; + orderByYear = false; + } else if (expectedExpiryDateFormat.indexOf(MonthShort + delimiter) === 0) { + useMonthPadding = false; + orderByYear = false; + } else { + orderByYear = true; + + // short form can match against long form, but long won't match against short + const containsLongMonthPattern = new RegExp(`${Month}`, "i"); + useMonthPadding = containsLongMonthPattern.test(expectedExpiryDateFormat); + } + + const containsLongYearPattern = new RegExp(`${Year}`, "i"); + + useYearFull = containsLongYearPattern.test(expectedExpiryDateFormat); + } + + const month = useMonthPadding + ? // Ensure zero-padding + ("0" + cardCipher.expMonth).slice(-2) + : // Handle zero-padded stored month values, even though they are not _expected_ to be as such + cardCipher.expMonth.replaceAll("0", ""); + // Note: assumes the user entered an `expYear` value with a length of either 2 or 4 + const year = (currentCentury + cardCipher.expYear).slice(useYearFull ? -4 : -2); + + const combinedExpiryFillValue = (orderByYear ? [year, month] : [month, year]).join(delimiter); + + return combinedExpiryFillValue; + } + + /** + * Returns a string value representation of discovered guidance for a combined month and year expiration value from the field attributes + * + * @param {AutofillField} field + */ + private getExpectedExpiryDateFormat( + field: AutofillField, + ): [string | null, CardExpiryDateFormat | null] { + let expectedDateFormat = null; + let dateFormatPatterns = null; + + const expiryDateDelimitersPattern = + "\\" + CreditCardAutoFillConstants.CardExpiryDateDelimiters.join("\\"); + + CreditCardAutoFillConstants.CardExpiryDateFormats.find((dateFormat) => { + dateFormatPatterns = dateFormat; + + const { Month, MonthShort, YearShort, Year } = dateFormat; + + // Non-exhaustive coverage of field guidances. Some uncovered edge cases: ". " delimiter, space-delimited delimiters ("mm / yyyy"). + // We should consider if added whitespace is for improved readability of user-guidance or actually desired in the filled value. + // e.g. "/((mm|m)[\/\-\.\ ]{0,1}(yyyy|yy))|((yyyy|yy)[\/\-\.\ ]{0,1}(mm|m))/gi" + const dateFormatPattern = new RegExp( + `((${Month}|${MonthShort})[${expiryDateDelimitersPattern}]{0,1}(${Year}|${YearShort}))|((${Year}|${YearShort})[${expiryDateDelimitersPattern}]{0,1}(${Month}|${MonthShort}))`, + "gi", + ); + + return CreditCardAutoFillConstants.CardAttributesExtended.find((attributeName) => { + const fieldAttributeValue = field[attributeName]; + + const fieldAttributeMatch = fieldAttributeValue?.match(dateFormatPattern); + // break find as soon as a match is found + + if (fieldAttributeMatch?.length) { + expectedDateFormat = fieldAttributeMatch[0]; + + // remove any irrelevant characters + const irrelevantExpiryCharactersPattern = new RegExp( + // "or digits" to ensure numbers are removed from guidance pattern, which aren't covered by ^\w + `[^\\w${expiryDateDelimitersPattern}]|[\\d]`, + "gi", + ); + expectedDateFormat.replaceAll(irrelevantExpiryCharactersPattern, ""); + + return true; + } + + return false; + }); + }); + + return [expectedDateFormat, dateFormatPatterns]; } /** @@ -1213,12 +1502,16 @@ export default class AutofillService implements AutofillServiceInterface { * @returns {AutofillScript} * @private */ - private generateIdentityFillScript( + private async generateIdentityFillScript( fillScript: AutofillScript, pageDetails: AutofillPageDetails, filledFields: { [id: string]: AutofillField }, options: GenerateFillScriptOptions, - ): AutofillScript { + ): Promise { + if (await this.configService.getFeatureFlag(FeatureFlag.GenerateIdentityFillScriptRefactor)) { + return this._generateIdentityFillScript(fillScript, pageDetails, filledFields, options); + } + if (!options.cipher.identity) { return null; } @@ -1226,7 +1519,10 @@ export default class AutofillService implements AutofillServiceInterface { const fillFields: { [id: string]: AutofillField } = {}; pageDetails.fields.forEach((f) => { - if (AutofillService.isExcludedFieldType(f, AutoFillConstants.ExcludedAutofillTypes)) { + if ( + AutofillService.isExcludedFieldType(f, AutoFillConstants.ExcludedAutofillTypes) || + ["current-password", "new-password"].includes(f.autoCompleteType) + ) { return; } @@ -1443,6 +1739,589 @@ export default class AutofillService implements AutofillServiceInterface { return fillScript; } + /** + * Generates the autofill script for the specified page details and identity cipher item. + * + * @param fillScript - Object to store autofill script, passed between method references + * @param pageDetails - The details of the page to autofill + * @param filledFields - The fields that have already been filled, passed between method references + * @param options - Contains data used to fill cipher items + */ + private _generateIdentityFillScript( + fillScript: AutofillScript, + pageDetails: AutofillPageDetails, + filledFields: { [id: string]: AutofillField }, + options: GenerateFillScriptOptions, + ): AutofillScript { + const identity = options.cipher.identity; + if (!identity) { + return null; + } + + for (let fieldsIndex = 0; fieldsIndex < pageDetails.fields.length; fieldsIndex++) { + const field = pageDetails.fields[fieldsIndex]; + if (this.excludeFieldFromIdentityFill(field)) { + continue; + } + + const keywordsList = this.getIdentityAutofillFieldKeywords(field); + const keywordsCombined = keywordsList.join(","); + if (this.shouldMakeIdentityTitleFillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.title, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityNameFillScript(filledFields, keywordsList)) { + this.makeIdentityNameFillScript(fillScript, filledFields, field, identity); + continue; + } + + if (this.shouldMakeIdentityFirstNameFillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.firstName, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityMiddleNameFillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.middleName, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityLastNameFillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.lastName, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityEmailFillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.email, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityAddressFillScript(filledFields, keywordsList)) { + this.makeIdentityAddressFillScript(fillScript, filledFields, field, identity); + continue; + } + + if (this.shouldMakeIdentityAddress1FillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.address1, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityAddress2FillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.address2, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityAddress3FillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.address3, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityPostalCodeFillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.postalCode, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityCityFillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.city, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityStateFillScript(filledFields, keywordsCombined)) { + this.makeIdentityStateFillScript(fillScript, filledFields, field, identity); + continue; + } + + if (this.shouldMakeIdentityCountryFillScript(filledFields, keywordsCombined)) { + this.makeIdentityCountryFillScript(fillScript, filledFields, field, identity); + continue; + } + + if (this.shouldMakeIdentityPhoneFillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.phone, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityUserNameFillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.username, field, filledFields); + continue; + } + + if (this.shouldMakeIdentityCompanyFillScript(filledFields, keywordsCombined)) { + this.makeScriptActionWithValue(fillScript, identity.company, field, filledFields); + } + } + + return fillScript; + } + + /** + * Identifies if the current field should be excluded from triggering autofill of the identity cipher. + * + * @param field - The field to check + */ + private excludeFieldFromIdentityFill(field: AutofillField): boolean { + return ( + AutofillService.isExcludedFieldType(field, AutoFillConstants.ExcludedAutofillTypes) || + AutoFillConstants.ExcludedIdentityAutocompleteTypes.has(field.autoCompleteType) || + !field.viewable + ); + } + + /** + * Gathers all unique keyword identifiers from a field that can be used to determine what + * identity value should be filled. + * + * @param field - The field to gather keywords from + */ + private getIdentityAutofillFieldKeywords(field: AutofillField): string[] { + const keywords: Set = new Set(); + for (let index = 0; index < IdentityAutoFillConstants.IdentityAttributes.length; index++) { + const attribute = IdentityAutoFillConstants.IdentityAttributes[index]; + if (field[attribute]) { + keywords.add( + field[attribute] + .trim() + .toLowerCase() + .replace(/[^a-zA-Z0-9]+/g, ""), + ); + } + } + + return Array.from(keywords); + } + + /** + * Identifies if a fill script action for the identity title + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityTitleFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.title && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.TitleFieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity name + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityNameFillScript( + filledFields: Record, + keywords: string[], + ): boolean { + return ( + !filledFields.name && + keywords.some((keyword) => + AutofillService.isFieldMatch( + keyword, + IdentityAutoFillConstants.FullNameFieldNames, + IdentityAutoFillConstants.FullNameFieldNameValues, + ), + ) + ); + } + + /** + * Identifies if a fill script action for the identity first name + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityFirstNameFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.firstName && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.FirstnameFieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity middle name + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityMiddleNameFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.middleName && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.MiddlenameFieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity last name + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityLastNameFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.lastName && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.LastnameFieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity email + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityEmailFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.email && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.EmailFieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity address + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityAddressFillScript( + filledFields: Record, + keywords: string[], + ): boolean { + return ( + !filledFields.address && + keywords.some((keyword) => + AutofillService.isFieldMatch( + keyword, + IdentityAutoFillConstants.AddressFieldNames, + IdentityAutoFillConstants.AddressFieldNameValues, + ), + ) + ); + } + + /** + * Identifies if a fill script action for the identity address1 + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityAddress1FillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.address1 && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.Address1FieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity address2 + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityAddress2FillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.address2 && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.Address2FieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity address3 + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityAddress3FillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.address3 && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.Address3FieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity postal code + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityPostalCodeFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.postalCode && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.PostalCodeFieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity city + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityCityFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.city && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.CityFieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity state + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityStateFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.state && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.StateFieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity country + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityCountryFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.country && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.CountryFieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity phone + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityPhoneFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.phone && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.PhoneFieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity username + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityUserNameFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.username && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.UserNameFieldNames) + ); + } + + /** + * Identifies if a fill script action for the identity company + * field should be created for the provided field. + * + * @param filledFields - The fields that have already been filled + * @param keywords - The keywords from the field + */ + private shouldMakeIdentityCompanyFillScript( + filledFields: Record, + keywords: string, + ): boolean { + return ( + !filledFields.company && + AutofillService.isFieldMatch(keywords, IdentityAutoFillConstants.CompanyFieldNames) + ); + } + + /** + * Creates an identity name fill script action for the provided field. This is used + * when filling a `full name` field, using the first, middle, and last name from the + * identity cipher item. + * + * @param fillScript - The autofill script to add the action to + * @param filledFields - The fields that have already been filled + * @param field - The field to fill + * @param identity - The identity cipher item + */ + private makeIdentityNameFillScript( + fillScript: AutofillScript, + filledFields: Record, + field: AutofillField, + identity: IdentityView, + ) { + let name = ""; + if (identity.firstName) { + name += identity.firstName; + } + + if (identity.middleName) { + name += !name ? identity.middleName : ` ${identity.middleName}`; + } + + if (identity.lastName) { + name += !name ? identity.lastName : ` ${identity.lastName}`; + } + + this.makeScriptActionWithValue(fillScript, name, field, filledFields); + } + + /** + * Creates an identity address fill script action for the provided field. This is used + * when filling a generic `address` field, using the address1, address2, and address3 + * from the identity cipher item. + * + * @param fillScript - The autofill script to add the action to + * @param filledFields - The fields that have already been filled + * @param field - The field to fill + * @param identity - The identity cipher item + */ + private makeIdentityAddressFillScript( + fillScript: AutofillScript, + filledFields: Record, + field: AutofillField, + identity: IdentityView, + ) { + if (!identity.address1) { + return; + } + + let address = identity.address1; + + if (identity.address2) { + address += `, ${identity.address2}`; + } + + if (identity.address3) { + address += `, ${identity.address3}`; + } + + this.makeScriptActionWithValue(fillScript, address, field, filledFields); + } + + /** + * Creates an identity state fill script action for the provided field. This is used + * when filling a `state` field, using the state value from the identity cipher item. + * If the state value is a full name, it will be converted to an ISO code. + * + * @param fillScript - The autofill script to add the action to + * @param filledFields - The fields that have already been filled + * @param field - The field to fill + * @param identity - The identity cipher item + */ + private makeIdentityStateFillScript( + fillScript: AutofillScript, + filledFields: Record, + field: AutofillField, + identity: IdentityView, + ) { + if (!identity.state) { + return; + } + + if (identity.state.length <= 2) { + this.makeScriptActionWithValue(fillScript, identity.state, field, filledFields); + return; + } + + const stateLower = identity.state.toLowerCase(); + const isoState = + IdentityAutoFillConstants.IsoStates[stateLower] || + IdentityAutoFillConstants.IsoProvinces[stateLower]; + if (isoState) { + this.makeScriptActionWithValue(fillScript, isoState, field, filledFields); + } + } + + /** + * Creates an identity country fill script action for the provided field. This is used + * when filling a `country` field, using the country value from the identity cipher item. + * If the country value is a full name, it will be converted to an ISO code. + * + * @param fillScript - The autofill script to add the action to + * @param filledFields - The fields that have already been filled + * @param field - The field to fill + * @param identity - The identity cipher item + */ + private makeIdentityCountryFillScript( + fillScript: AutofillScript, + filledFields: Record, + field: AutofillField, + identity: IdentityView, + ) { + if (!identity.country) { + return; + } + + if (identity.country.length <= 2) { + this.makeScriptActionWithValue(fillScript, identity.country, field, filledFields); + return; + } + + const countryLower = identity.country.toLowerCase(); + const isoCountry = IdentityAutoFillConstants.IsoCountries[countryLower]; + if (isoCountry) { + this.makeScriptActionWithValue(fillScript, isoCountry, field, filledFields); + } + } + /** * Accepts an HTMLInputElement type value and a list of * excluded types and returns true if the type is excluded. @@ -2162,8 +3041,8 @@ export default class AutofillService implements AutofillServiceInterface { if (!inlineMenuPreviouslyDisabled && !inlineMenuCurrentlyDisabled) { const tabs = await BrowserApi.tabsQuery({}); tabs.forEach((tab) => - BrowserApi.tabSendMessageData(tab, "updateAutofillOverlayVisibility", { - autofillOverlayVisibility: currentSetting, + BrowserApi.tabSendMessageData(tab, "updateAutofillInlineMenuVisibility", { + inlineMenuVisibility: currentSetting, }), ); return; 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 9bb0e717a26..97b231a1dac 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 @@ -11,9 +11,11 @@ import { FormElementWithAttribute, } from "../types"; -import AutofillOverlayContentService from "./autofill-overlay-content.service"; -import CollectAutofillContentService from "./collect-autofill-content.service"; +import { InlineMenuFieldQualificationService } from "./abstractions/inline-menu-field-qualifications.service"; +import { AutofillOverlayContentService } from "./autofill-overlay-content.service"; +import { CollectAutofillContentService } from "./collect-autofill-content.service"; import DomElementVisibilityService from "./dom-element-visibility.service"; +import { DomQueryService } from "./dom-query.service"; const mockLoginForm = `
@@ -28,7 +30,12 @@ const waitForIdleCallback = () => new Promise((resolve) => globalThis.requestIdl describe("CollectAutofillContentService", () => { const domElementVisibilityService = new DomElementVisibilityService(); - const autofillOverlayContentService = new AutofillOverlayContentService(); + const inlineMenuFieldQualificationService = mock(); + const domQueryService = new DomQueryService(); + const autofillOverlayContentService = new AutofillOverlayContentService( + domQueryService, + inlineMenuFieldQualificationService, + ); let collectAutofillContentService: CollectAutofillContentService; const mockIntersectionObserver = mock(); const mockQuerySelectorAll = mockQuerySelectorAllDefinedCall(); @@ -39,6 +46,7 @@ describe("CollectAutofillContentService", () => { document.body.innerHTML = mockLoginForm; collectAutofillContentService = new CollectAutofillContentService( domElementVisibilityService, + domQueryService, autofillOverlayContentService, ); window.IntersectionObserver = jest.fn(() => mockIntersectionObserver); @@ -151,7 +159,7 @@ describe("CollectAutofillContentService", () => { "data-stripe": null, }; collectAutofillContentService["domRecentlyMutated"] = false; - collectAutofillContentService["autofillFormElements"] = new Map([ + collectAutofillContentService["_autofillFormElements"] = new Map([ [formElement, autofillForm], ]); collectAutofillContentService["autofillFieldElements"] = new Map([ @@ -239,7 +247,7 @@ describe("CollectAutofillContentService", () => { "data-stripe": null, }; collectAutofillContentService["domRecentlyMutated"] = false; - collectAutofillContentService["autofillFormElements"] = new Map([ + collectAutofillContentService["_autofillFormElements"] = new Map([ [formElement, autofillForm], ]); collectAutofillContentService["autofillFieldElements"] = new Map([ @@ -250,7 +258,7 @@ describe("CollectAutofillContentService", () => { .mockResolvedValue(true); const setupAutofillOverlayListenerOnFieldSpy = jest.spyOn( collectAutofillContentService["autofillOverlayContentService"], - "setupAutofillOverlayListenerOnField", + "setupOverlayListeners", ); await collectAutofillContentService.getPageDetails(); @@ -453,51 +461,6 @@ describe("CollectAutofillContentService", () => { }); }); - describe("deepQueryElements", () => { - beforeEach(() => { - collectAutofillContentService["mutationObserver"] = mock(); - }); - - it("queries form field elements that are nested within a ShadowDOM", () => { - const root = document.createElement("div"); - const shadowRoot = root.attachShadow({ mode: "open" }); - const form = document.createElement("form"); - const input = document.createElement("input"); - input.type = "text"; - form.appendChild(input); - shadowRoot.appendChild(form); - - const formFieldElements = collectAutofillContentService.deepQueryElements( - shadowRoot, - "input", - true, - ); - - expect(formFieldElements).toStrictEqual([input]); - }); - - it("queries form field elements that are nested within multiple ShadowDOM elements", () => { - const root = document.createElement("div"); - const shadowRoot1 = root.attachShadow({ mode: "open" }); - const root2 = document.createElement("div"); - const shadowRoot2 = root2.attachShadow({ mode: "open" }); - const form = document.createElement("form"); - const input = document.createElement("input"); - input.type = "text"; - form.appendChild(input); - shadowRoot2.appendChild(form); - shadowRoot1.appendChild(root2); - - const formFieldElements = collectAutofillContentService.deepQueryElements( - shadowRoot1, - "input", - true, - ); - - expect(formFieldElements).toStrictEqual([input]); - }); - }); - describe("buildAutofillFormsData", () => { it("will not attempt to gather data from a cached form element", () => { const documentTitle = "Test Page"; @@ -523,7 +486,7 @@ describe("CollectAutofillContentService", () => { htmlID: formId, htmlMethod: formMethod, }; - collectAutofillContentService["autofillFormElements"] = new Map([ + collectAutofillContentService["_autofillFormElements"] = new Map([ [formElement, existingAutofillForm], ]); const formElements = Array.from(document.querySelectorAll("form")); @@ -2131,7 +2094,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], ]); @@ -2154,7 +2117,7 @@ describe("CollectAutofillContentService", () => { ]); await waitForIdleCallback(); - expect(collectAutofillContentService["autofillFormElements"].size).toEqual(0); + expect(collectAutofillContentService["_autofillFormElements"].size).toEqual(0); expect(collectAutofillContentService["autofillFieldElements"].size).toEqual(0); }); @@ -2276,13 +2239,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", () => { @@ -2328,7 +2291,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); }); }); @@ -2375,7 +2338,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); @@ -2447,14 +2412,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, @@ -2462,7 +2427,7 @@ describe("CollectAutofillContentService", () => { autofillForm, ); - expect(collectAutofillContentService["autofillFormElements"].set).toBeCalledWith( + expect(collectAutofillContentService["_autofillFormElements"].set).toBeCalledWith( formElement, autofillForm, ); @@ -2470,7 +2435,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", @@ -2478,7 +2443,7 @@ describe("CollectAutofillContentService", () => { autofillForm, ); - expect(collectAutofillContentService["autofillFormElements"].set).not.toBeCalled(); + expect(collectAutofillContentService["_autofillFormElements"].set).not.toBeCalled(); }); }); @@ -2564,7 +2529,7 @@ describe("CollectAutofillContentService", () => { ); setupAutofillOverlayListenerOnFieldSpy = jest.spyOn( collectAutofillContentService["autofillOverlayContentService"], - "setupAutofillOverlayListenerOnField", + "setupOverlayListeners", ); }); @@ -2585,9 +2550,11 @@ describe("CollectAutofillContentService", () => { it("skips setting up the overlay listeners on a field that is not viewable", async () => { const formFieldElement = document.createElement("input") as ElementWithOpId; + const autofillField = mock(); const entries = [ { target: formFieldElement, isIntersecting: true }, ] as unknown as IntersectionObserverEntry[]; + collectAutofillContentService["autofillFieldElements"].set(formFieldElement, autofillField); isFormFieldViewableSpy.mockReturnValueOnce(false); await collectAutofillContentService["handleFormElementIntersection"](entries); @@ -2596,7 +2563,21 @@ describe("CollectAutofillContentService", () => { expect(setupAutofillOverlayListenerOnFieldSpy).not.toHaveBeenCalled(); }); - it("sets up the overlay listeners on a viewable field", async () => { + it("skips setting up the inline menu listeners if the observed form field is not present in the cache", async () => { + const formFieldElement = document.createElement("input") as ElementWithOpId; + const entries = [ + { target: formFieldElement, isIntersecting: true }, + ] as unknown as IntersectionObserverEntry[]; + isFormFieldViewableSpy.mockReturnValueOnce(true); + collectAutofillContentService["intersectionObserver"] = mockIntersectionObserver; + + await collectAutofillContentService["handleFormElementIntersection"](entries); + + expect(isFormFieldViewableSpy).not.toHaveBeenCalled(); + expect(setupAutofillOverlayListenerOnFieldSpy).not.toHaveBeenCalled(); + }); + + it("sets up the inline menu listeners on a viewable field", async () => { const formFieldElement = document.createElement("input") as ElementWithOpId; const autofillField = mock(); const entries = [ @@ -2616,4 +2597,17 @@ describe("CollectAutofillContentService", () => { ); }); }); + + describe("destroy", () => { + it("clears the updateAfterMutationIdleCallback", () => { + jest.spyOn(window, "clearTimeout"); + collectAutofillContentService["updateAfterMutationIdleCallback"] = setTimeout(jest.fn, 100); + + collectAutofillContentService.destroy(); + + expect(clearTimeout).toHaveBeenCalledWith( + collectAutofillContentService["updateAfterMutationIdleCallback"], + ); + }); + }); }); 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 75c564e868e..efacafbe88e 100644 --- a/apps/browser/src/autofill/services/collect-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/collect-autofill-content.service.ts @@ -1,12 +1,7 @@ import AutofillField from "../models/autofill-field"; import AutofillForm from "../models/autofill-form"; import AutofillPageDetails from "../models/autofill-page-details"; -import { - ElementWithOpId, - FillableFormFieldElement, - FormElementWithAttribute, - FormFieldElement, -} from "../types"; +import { ElementWithOpId, FillableFormFieldElement, FormFieldElement } from "../types"; import { elementIsDescriptionDetailsElement, elementIsDescriptionTermElement, @@ -20,7 +15,9 @@ import { elementIsTextAreaElement, nodeIsFormElement, nodeIsInputElement, - // sendExtensionMessage, + sendExtensionMessage, + getAttributeBoolean, + getPropertyOrAttribute, requestIdleCallbackPolyfill, cancelIdleCallbackPolyfill, } from "../utils"; @@ -33,13 +30,15 @@ import { UpdateAutofillDataAttributeParams, } from "./abstractions/collect-autofill-content.service"; import { DomElementVisibilityService } from "./abstractions/dom-element-visibility.service"; +import { DomQueryService } from "./abstractions/dom-query.service"; -class CollectAutofillContentService implements CollectAutofillContentServiceInterface { - private readonly domElementVisibilityService: DomElementVisibilityService; - private readonly autofillOverlayContentService: AutofillOverlayContentService; +export class CollectAutofillContentService implements CollectAutofillContentServiceInterface { + private readonly sendExtensionMessage = sendExtensionMessage; + private readonly getAttributeBoolean = getAttributeBoolean; + 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; @@ -61,12 +60,10 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte private useTreeWalkerStrategyFlagSet = true; constructor( - domElementVisibilityService: DomElementVisibilityService, - autofillOverlayContentService?: AutofillOverlayContentService, + private domElementVisibilityService: DomElementVisibilityService, + private domQueryService: DomQueryService, + private autofillOverlayContentService?: AutofillOverlayContentService, ) { - this.domElementVisibilityService = domElementVisibilityService; - this.autofillOverlayContentService = autofillOverlayContentService; - let inputQuery = "input:not([data-bwignore])"; for (const type of this.ignoredInputTypes) { inputQuery += `:not([type="${type}"])`; @@ -79,6 +76,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 @@ -122,7 +123,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte this.domRecentlyMutated = false; const pageDetails = this.getFormattedPageDetails(autofillFormsData, autofillFieldsData); - this.setupInlineMenuListeners(pageDetails); + this.setupOverlayListeners(pageDetails); return pageDetails; } @@ -157,89 +158,6 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte return fieldElementsWithOpid[0]; } - /** - * Queries all elements in the DOM that match the given query string. - * Also, recursively queries all shadow roots for the element. - * - * @param root - The root element to start the query from - * @param queryString - The query string to match elements against - * @param isObservingShadowRoot - Determines whether to observe shadow roots - */ - deepQueryElements( - root: Document | ShadowRoot | Element, - queryString: string, - isObservingShadowRoot = false, - ): T[] { - let elements = this.queryElements(root, queryString); - const shadowRoots = this.recursivelyQueryShadowRoots(root, isObservingShadowRoot); - for (let index = 0; index < shadowRoots.length; index++) { - const shadowRoot = shadowRoots[index]; - elements = elements.concat(this.queryElements(shadowRoot, queryString)); - } - - return elements; - } - - /** - * Queries the DOM for elements based on the given query string. - * - * @param root - The root element to start the query from - * @param queryString - The query string to match elements against - */ - private queryElements(root: Document | ShadowRoot | Element, queryString: string): T[] { - if (!root.querySelector(queryString)) { - return []; - } - - return Array.from(root.querySelectorAll(queryString)) as T[]; - } - - /** - * Recursively queries all shadow roots found within the given root element. - * Will also set up a mutation observer on the shadow root if the - * `isObservingShadowRoot` parameter is set to true. - * - * @param root - The root element to start the query from - * @param isObservingShadowRoot - Determines whether to observe shadow roots - */ - private recursivelyQueryShadowRoots( - root: Document | ShadowRoot | Element, - isObservingShadowRoot = false, - ): ShadowRoot[] { - let shadowRoots = this.queryShadowRoots(root); - for (let index = 0; index < shadowRoots.length; index++) { - const shadowRoot = shadowRoots[index]; - shadowRoots = shadowRoots.concat(this.recursivelyQueryShadowRoots(shadowRoot)); - if (isObservingShadowRoot) { - this.mutationObserver.observe(shadowRoot, { - attributes: true, - childList: true, - subtree: true, - }); - } - } - - return shadowRoots; - } - - /** - * Queries any immediate shadow roots found within the given root element. - * - * @param root - The root element to start the query from - */ - private queryShadowRoots(root: Document | ShadowRoot | Element): ShadowRoot[] { - const shadowRoots: ShadowRoot[] = []; - const potentialShadowRoots = root.querySelectorAll(":defined"); - for (let index = 0; index < potentialShadowRoots.length; index++) { - const shadowRoot = this.getShadowRoot(potentialShadowRoots[index]); - if (shadowRoot) { - shadowRoots.push(shadowRoot); - } - } - - return shadowRoots; - } - /** * Sorts the AutofillFieldElements map by the elementNumber property. * @private @@ -286,7 +204,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte autofillField.viewable = await this.domElementVisibilityService.isFormFieldViewable(element); if (!previouslyViewable && autofillField.viewable) { - this.setupInlineMenuListenerOnField(element, autofillField); + this.setupOverlayOnField(element, autofillField); } }); } @@ -302,14 +220,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 +258,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; @@ -381,7 +299,11 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte if (!formFieldElements) { formFieldElements = this.useTreeWalkerStrategyFlagSet ? this.queryTreeWalkerForAutofillFormFieldElements() - : this.deepQueryElements(document, this.formFieldQueryString, true); + : this.domQueryService.deepQueryElements( + document, + this.formFieldQueryString, + this.mutationObserver, + ); } if (!fieldsLimit || formFieldElements.length <= fieldsLimit) { @@ -537,26 +459,6 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte ); } - /** - * Returns a boolean representing the attribute value of an element. - * @param {ElementWithOpId} element - * @param {string} attributeName - * @param {boolean} checkString - * @returns {boolean} - * @private - */ - private getAttributeBoolean( - element: ElementWithOpId, - attributeName: string, - checkString = false, - ): boolean { - if (checkString) { - return this.getPropertyOrAttribute(element, attributeName) === "true"; - } - - return Boolean(this.getPropertyOrAttribute(element, attributeName)); - } - /** * Returns the attribute of an element as a lowercase value. * @param {ElementWithOpId} element @@ -742,6 +644,10 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte } const tableDataElementIndex = tableDataElement.cellIndex; + if (tableDataElementIndex < 0) { + return null; + } + const parentSiblingTableRowElement = tableDataElement.closest("tr") ?.previousElementSibling as HTMLTableRowElement; @@ -868,21 +774,6 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte return this.recursivelyGetTextFromPreviousSiblings(siblingElement); } - /** - * Get the value of a property or attribute from a FormFieldElement. - * @param {HTMLElement} element - * @param {string} attributeName - * @returns {string | null} - * @private - */ - private getPropertyOrAttribute(element: HTMLElement, attributeName: string): string | null { - if (attributeName in element) { - return (element as FormElementWithAttribute)[attributeName]; - } - - return element.getAttribute(attributeName); - } - /** * Gets the value of the element. If the element is a checkbox, returns a checkmark if the * checkbox is checked, or an empty string if it is not checked. If the element is a hidden @@ -949,10 +840,10 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte return this.queryTreeWalkerForAutofillFormAndFieldElements(); } - const queriedElements = this.deepQueryElements( + const queriedElements = this.domQueryService.deepQueryElements( document, `form, ${this.formFieldQueryString}`, - true, + this.mutationObserver, ); const formElements: HTMLFormElement[] = []; const formFieldElements: FormFieldElement[] = []; @@ -1073,10 +964,11 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte this.domRecentlyMutated = true; if (this.autofillOverlayContentService) { this.autofillOverlayContentService.pageDetailsUpdateRequired = true; + void this.sendExtensionMessage("closeAutofillInlineMenu", { forceCloseInlineMenu: true }); } this.noFieldsFound = false; - this.autofillFormElements.clear(); + this._autofillFormElements.clear(); this.autofillFieldElements.clear(); this.updateAutofillElementsAfterMutation(); @@ -1153,7 +1045,11 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte const autofillElements = this.useTreeWalkerStrategyFlagSet ? this.queryTreeWalkerForMutatedElements(node) - : this.deepQueryElements(node, `form, ${this.formFieldQueryString}`, true); + : this.domQueryService.deepQueryElements( + node, + `form, ${this.formFieldQueryString}`, + this.mutationObserver, + ); if (autofillElements.length) { mutatedElements = mutatedElements.concat(autofillElements); } @@ -1212,8 +1108,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; } @@ -1250,7 +1146,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte } const attributeName = mutation.attributeName?.toLowerCase(); - const autofillForm = this.autofillFormElements.get( + const autofillForm = this._autofillFormElements.get( targetElement as ElementWithOpId, ); @@ -1305,8 +1201,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); } } @@ -1411,20 +1307,20 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte continue; } + const cachedAutofillFieldElement = this.autofillFieldElements.get(formFieldElement); + if (!cachedAutofillFieldElement) { + this.intersectionObserver.unobserve(entry.target); + continue; + } + const isViewable = await this.domElementVisibilityService.isFormFieldViewable(formFieldElement); if (!isViewable) { continue; } - const cachedAutofillFieldElement = this.autofillFieldElements.get(formFieldElement); - if (!cachedAutofillFieldElement) { - continue; - } - cachedAutofillFieldElement.viewable = true; - - this.setupInlineMenuListenerOnField(formFieldElement, cachedAutofillFieldElement); + this.setupOverlayOnField(formFieldElement, cachedAutofillFieldElement); this.intersectionObserver?.unobserve(entry.target); } @@ -1435,14 +1331,12 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte * * @param pageDetails - The page details to use for the inline menu listeners */ - private setupInlineMenuListeners(pageDetails: AutofillPageDetails) { - if (!this.autofillOverlayContentService) { - return; + private setupOverlayListeners(pageDetails: AutofillPageDetails) { + if (this.autofillOverlayContentService) { + this.autofillFieldElements.forEach((autofillField, formFieldElement) => { + this.setupOverlayOnField(formFieldElement, autofillField, pageDetails); + }); } - - this.autofillFieldElements.forEach((autofillField, formFieldElement) => { - this.setupInlineMenuListenerOnField(formFieldElement, autofillField, pageDetails); - }); } /** @@ -1452,27 +1346,25 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte * @param autofillField - The metadata for the form field * @param pageDetails - The page details to use for the inline menu listeners */ - private setupInlineMenuListenerOnField( + private setupOverlayOnField( formFieldElement: ElementWithOpId, autofillField: AutofillField, pageDetails?: AutofillPageDetails, ) { - if (!this.autofillOverlayContentService) { - return; - } + if (this.autofillOverlayContentService) { + const autofillPageDetails = + pageDetails || + this.getFormattedPageDetails( + this.getFormattedAutofillFormsData(), + this.getFormattedAutofillFieldsData(), + ); - const autofillPageDetails = - pageDetails || - this.getFormattedPageDetails( - this.getFormattedAutofillFormsData(), - this.getFormattedAutofillFieldsData(), + void this.autofillOverlayContentService.setupOverlayListeners( + formFieldElement, + autofillField, + autofillPageDetails, ); - - void this.autofillOverlayContentService.setupAutofillOverlayListenerOnField( - formFieldElement, - autofillField, - autofillPageDetails, - ); + } } /** @@ -1487,78 +1379,6 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte this.intersectionObserver?.disconnect(); } - /** - * Queries the DOM for all the nodes that match the given filter callback - * and returns a collection of nodes. - * @param rootNode - * @param filterCallback - * @param isObservingShadowRoot - * - * @deprecated - This method remains as a fallback in the case that the deepQuery implementation fails. - */ - private queryAllTreeWalkerNodes( - rootNode: Node, - filterCallback: CallableFunction, - isObservingShadowRoot = true, - ): Node[] { - const treeWalkerQueryResults: Node[] = []; - - this.buildTreeWalkerNodesQueryResults( - rootNode, - treeWalkerQueryResults, - filterCallback, - isObservingShadowRoot, - ); - - return treeWalkerQueryResults; - } - - /** - * Recursively builds a collection of nodes that match the given filter callback. - * If a node has a ShadowRoot, it will be observed for mutations. - * - * @param rootNode - * @param treeWalkerQueryResults - * @param filterCallback - * - * @deprecated - This method remains as a fallback in the case that the deepQuery implementation fails. - */ - private buildTreeWalkerNodesQueryResults( - rootNode: Node, - treeWalkerQueryResults: Node[], - filterCallback: CallableFunction, - isObservingShadowRoot: boolean, - ) { - const treeWalker = document?.createTreeWalker(rootNode, NodeFilter.SHOW_ELEMENT); - let currentNode = treeWalker?.currentNode; - - while (currentNode) { - if (filterCallback(currentNode)) { - treeWalkerQueryResults.push(currentNode); - } - - const nodeShadowRoot = this.getShadowRoot(currentNode); - if (nodeShadowRoot) { - if (isObservingShadowRoot) { - this.mutationObserver.observe(nodeShadowRoot, { - attributes: true, - childList: true, - subtree: true, - }); - } - - this.buildTreeWalkerNodesQueryResults( - nodeShadowRoot, - treeWalkerQueryResults, - filterCallback, - isObservingShadowRoot, - ); - } - - currentNode = treeWalker?.nextNode(); - } - } - /** * @deprecated - This method remains as a fallback in the case that the deepQuery implementation fails. */ @@ -1568,19 +1388,23 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte } { const formElements: HTMLFormElement[] = []; const formFieldElements: FormFieldElement[] = []; - this.queryAllTreeWalkerNodes(document.documentElement, (node: Node) => { - if (nodeIsFormElement(node)) { - formElements.push(node); - return true; - } + this.domQueryService.queryAllTreeWalkerNodes( + document.documentElement, + (node: Node) => { + if (nodeIsFormElement(node)) { + formElements.push(node); + return true; + } - if (this.isNodeFormFieldElement(node)) { - formFieldElements.push(node as FormFieldElement); - return true; - } + if (this.isNodeFormFieldElement(node)) { + formFieldElements.push(node as FormFieldElement); + return true; + } - return false; - }); + return false; + }, + this.mutationObserver, + ); return { formElements, formFieldElements }; } @@ -1589,8 +1413,10 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte * @deprecated - This method remains as a fallback in the case that the deepQuery implementation fails. */ private queryTreeWalkerForAutofillFormFieldElements(): FormFieldElement[] { - return this.queryAllTreeWalkerNodes(document.documentElement, (node: Node) => - this.isNodeFormFieldElement(node), + return this.domQueryService.queryAllTreeWalkerNodes( + document.documentElement, + (node: Node) => this.isNodeFormFieldElement(node), + this.mutationObserver, ) as FormFieldElement[]; } @@ -1600,10 +1426,11 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte * @param node - The node to query */ private queryTreeWalkerForMutatedElements(node: Node): HTMLElement[] { - return this.queryAllTreeWalkerNodes( + return this.domQueryService.queryAllTreeWalkerNodes( node, (walkerNode: Node) => nodeIsFormElement(walkerNode) || this.isNodeFormFieldElement(walkerNode), + this.mutationObserver, ) as HTMLElement[]; } @@ -1611,10 +1438,9 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte * @deprecated - This method remains as a fallback in the case that the deepQuery implementation fails. */ private queryTreeWalkerForPasswordElements(): HTMLElement[] { - return this.queryAllTreeWalkerNodes( + return this.domQueryService.queryAllTreeWalkerNodes( document.documentElement, (node: Node) => nodeIsInputElement(node) && node.type === "password", - false, ) as HTMLElement[]; } @@ -1628,8 +1454,8 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte return Boolean(this.queryTreeWalkerForPasswordElements()?.length); } - return Boolean(this.deepQueryElements(document, `input[type="password"]`)?.length); + return Boolean( + this.domQueryService.deepQueryElements(document, `input[type="password"]`)?.length, + ); } } - -export default CollectAutofillContentService; diff --git a/apps/browser/src/autofill/services/dom-element-visibility.service.ts b/apps/browser/src/autofill/services/dom-element-visibility.service.ts index 127ce84d919..9df4ccb8fbc 100644 --- a/apps/browser/src/autofill/services/dom-element-visibility.service.ts +++ b/apps/browser/src/autofill/services/dom-element-visibility.service.ts @@ -1,10 +1,13 @@ +import { AutofillInlineMenuContentService } from "../overlay/inline-menu/abstractions/autofill-inline-menu-content.service"; import { FillableFormFieldElement, FormFieldElement } from "../types"; -import { DomElementVisibilityService as domElementVisibilityServiceInterface } from "./abstractions/dom-element-visibility.service"; +import { DomElementVisibilityService as DomElementVisibilityServiceInterface } from "./abstractions/dom-element-visibility.service"; -class DomElementVisibilityService implements domElementVisibilityServiceInterface { +class DomElementVisibilityService implements DomElementVisibilityServiceInterface { private cachedComputedStyle: CSSStyleDeclaration | null = null; + constructor(private inlineMenuElements?: AutofillInlineMenuContentService) {} + /** * Checks if a form field is viewable. This is done by checking if the element is within the * viewport bounds, not hidden by CSS, and not hidden behind another element. @@ -187,6 +190,10 @@ class DomElementVisibilityService implements domElementVisibilityServiceInterfac return true; } + if (this.inlineMenuElements?.isElementInlineMenu(elementAtCenterPoint as HTMLElement)) { + return true; + } + const targetElementLabelsSet = new Set((targetElement as FillableFormFieldElement).labels); if (targetElementLabelsSet.has(elementAtCenterPoint as HTMLLabelElement)) { return true; diff --git a/apps/browser/src/autofill/services/dom-query.service.spec.ts b/apps/browser/src/autofill/services/dom-query.service.spec.ts new file mode 100644 index 00000000000..22212333fc8 --- /dev/null +++ b/apps/browser/src/autofill/services/dom-query.service.spec.ts @@ -0,0 +1,82 @@ +import { mockQuerySelectorAllDefinedCall } from "../spec/testing-utils"; + +import { DomQueryService } from "./dom-query.service"; + +describe("DomQueryService", () => { + let domQueryService: DomQueryService; + let mutationObserver: MutationObserver; + const mockQuerySelectorAll = mockQuerySelectorAllDefinedCall(); + + beforeEach(() => { + domQueryService = new DomQueryService(); + mutationObserver = new MutationObserver(() => {}); + }); + + afterAll(() => { + mockQuerySelectorAll.mockRestore(); + }); + + describe("deepQueryElements", () => { + it("queries form field elements that are nested within a ShadowDOM", () => { + const root = document.createElement("div"); + const shadowRoot = root.attachShadow({ mode: "open" }); + const form = document.createElement("form"); + const input = document.createElement("input"); + input.type = "text"; + form.appendChild(input); + shadowRoot.appendChild(form); + + const formFieldElements = domQueryService.deepQueryElements( + shadowRoot, + "input", + mutationObserver, + ); + + expect(formFieldElements).toStrictEqual([input]); + }); + + it("queries form field elements that are nested within multiple ShadowDOM elements", () => { + const root = document.createElement("div"); + const shadowRoot1 = root.attachShadow({ mode: "open" }); + const root2 = document.createElement("div"); + const shadowRoot2 = root2.attachShadow({ mode: "open" }); + const form = document.createElement("form"); + const input = document.createElement("input"); + input.type = "text"; + form.appendChild(input); + shadowRoot2.appendChild(form); + shadowRoot1.appendChild(root2); + + const formFieldElements = domQueryService.deepQueryElements( + shadowRoot1, + "input", + mutationObserver, + ); + + expect(formFieldElements).toStrictEqual([input]); + }); + }); + + describe("queryAllTreeWalkerNodes", () => { + it("queries form field elements that are nested within multiple ShadowDOM elements", () => { + const root = document.createElement("div"); + const shadowRoot1 = root.attachShadow({ mode: "open" }); + const root2 = document.createElement("div"); + const shadowRoot2 = root2.attachShadow({ mode: "open" }); + const form = document.createElement("form"); + const input = document.createElement("input"); + input.type = "text"; + form.appendChild(input); + shadowRoot2.appendChild(form); + shadowRoot1.appendChild(root2); + + const formFieldElements = domQueryService.queryAllTreeWalkerNodes( + shadowRoot1, + (element: Element) => element.tagName === "INPUT", + mutationObserver, + ); + + expect(formFieldElements).toStrictEqual([input]); + }); + }); +}); diff --git a/apps/browser/src/autofill/services/dom-query.service.ts b/apps/browser/src/autofill/services/dom-query.service.ts new file mode 100644 index 00000000000..0d766ea3ba0 --- /dev/null +++ b/apps/browser/src/autofill/services/dom-query.service.ts @@ -0,0 +1,185 @@ +import { nodeIsElement } from "../utils"; + +import { DomQueryService as DomQueryServiceInterface } from "./abstractions/dom-query.service"; + +export class DomQueryService implements DomQueryServiceInterface { + /** + * Queries all elements in the DOM that match the given query string. + * Also, recursively queries all shadow roots for the element. + * + * @param root - The root element to start the query from + * @param queryString - The query string to match elements against + * @param mutationObserver - The MutationObserver to use for observing shadow roots + */ + deepQueryElements( + root: Document | ShadowRoot | Element, + queryString: string, + mutationObserver?: MutationObserver, + ): T[] { + let elements = this.queryElements(root, queryString); + const shadowRoots = this.recursivelyQueryShadowRoots(root, mutationObserver); + for (let index = 0; index < shadowRoots.length; index++) { + const shadowRoot = shadowRoots[index]; + elements = elements.concat(this.queryElements(shadowRoot, queryString)); + } + + return elements; + } + + /** + * Queries the DOM for elements based on the given query string. + * + * @param root - The root element to start the query from + * @param queryString - The query string to match elements against + */ + private queryElements(root: Document | ShadowRoot | Element, queryString: string): T[] { + if (!root.querySelector(queryString)) { + return []; + } + + return Array.from(root.querySelectorAll(queryString)) as T[]; + } + + /** + * Recursively queries all shadow roots found within the given root element. + * Will also set up a mutation observer on the shadow root if the + * `isObservingShadowRoot` parameter is set to true. + * + * @param root - The root element to start the query from + * @param mutationObserver - The MutationObserver to use for observing shadow roots + */ + private recursivelyQueryShadowRoots( + root: Document | ShadowRoot | Element, + mutationObserver?: MutationObserver, + ): ShadowRoot[] { + let shadowRoots = this.queryShadowRoots(root); + for (let index = 0; index < shadowRoots.length; index++) { + const shadowRoot = shadowRoots[index]; + shadowRoots = shadowRoots.concat(this.recursivelyQueryShadowRoots(shadowRoot)); + if (mutationObserver) { + mutationObserver.observe(shadowRoot, { + attributes: true, + childList: true, + subtree: true, + }); + } + } + + return shadowRoots; + } + + /** + * Queries any immediate shadow roots found within the given root element. + * + * @param root - The root element to start the query from + */ + private queryShadowRoots(root: Document | ShadowRoot | Element): ShadowRoot[] { + const shadowRoots: ShadowRoot[] = []; + const potentialShadowRoots = root.querySelectorAll(":defined"); + for (let index = 0; index < potentialShadowRoots.length; index++) { + const shadowRoot = this.getShadowRoot(potentialShadowRoots[index]); + if (shadowRoot) { + shadowRoots.push(shadowRoot); + } + } + + return shadowRoots; + } + + /** + * Attempts to get the ShadowRoot of the passed node. If support for the + * extension based openOrClosedShadowRoot API is available, it will be used. + * Will return null if the node is not an HTMLElement or if the node has + * child nodes. + * + * @param {Node} node + */ + private getShadowRoot(node: Node): ShadowRoot | null { + if (!nodeIsElement(node)) { + return null; + } + + if (node.shadowRoot) { + return node.shadowRoot; + } + + if ((chrome as any).dom?.openOrClosedShadowRoot) { + try { + return (chrome as any).dom.openOrClosedShadowRoot(node); + } catch (error) { + return null; + } + } + + return (node as any).openOrClosedShadowRoot; + } + + /** + * Queries the DOM for all the nodes that match the given filter callback + * and returns a collection of nodes. + * @param rootNode + * @param filterCallback + * @param mutationObserver + */ + queryAllTreeWalkerNodes( + rootNode: Node, + filterCallback: CallableFunction, + mutationObserver?: MutationObserver, + ): Node[] { + const treeWalkerQueryResults: Node[] = []; + + this.buildTreeWalkerNodesQueryResults( + rootNode, + treeWalkerQueryResults, + filterCallback, + mutationObserver, + ); + + return treeWalkerQueryResults; + } + + /** + * Recursively builds a collection of nodes that match the given filter callback. + * If a node has a ShadowRoot, it will be observed for mutations. + * + * @param rootNode + * @param treeWalkerQueryResults + * @param filterCallback + * @param mutationObserver + */ + private buildTreeWalkerNodesQueryResults( + rootNode: Node, + treeWalkerQueryResults: Node[], + filterCallback: CallableFunction, + mutationObserver?: MutationObserver, + ) { + const treeWalker = document?.createTreeWalker(rootNode, NodeFilter.SHOW_ELEMENT); + let currentNode = treeWalker?.currentNode; + + while (currentNode) { + if (filterCallback(currentNode)) { + treeWalkerQueryResults.push(currentNode); + } + + const nodeShadowRoot = this.getShadowRoot(currentNode); + if (nodeShadowRoot) { + if (mutationObserver) { + mutationObserver.observe(nodeShadowRoot, { + attributes: true, + childList: true, + subtree: true, + }); + } + + this.buildTreeWalkerNodesQueryResults( + nodeShadowRoot, + treeWalkerQueryResults, + filterCallback, + mutationObserver, + ); + } + + currentNode = treeWalker?.nextNode(); + } + } +} diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts index 0f95cd527ee..f38994e9852 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts @@ -21,12 +21,29 @@ describe("InlineMenuFieldQualificationService", () => { }); describe("isFieldForLoginForm", () => { + it("disqualifies totp fields", () => { + const field = mock({ + type: "text", + autoCompleteType: "one-time-code", + htmlName: "totp", + htmlID: "totp", + placeholder: "totp", + }); + + expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe( + false, + ); + }); + describe("qualifying a password field for a login form", () => { describe("an invalid password field", () => { it("has a `new-password` autoCompleteType", () => { const field = mock({ type: "password", autoCompleteType: "new-password", + htmlName: "input-password", + htmlID: "input-password", + placeholder: "input-password", }); expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe( @@ -39,6 +56,8 @@ describe("InlineMenuFieldQualificationService", () => { type: "password", placeholder: "create account password", autoCompleteType: "", + htmlName: "input-password", + htmlID: "input-password", }); expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe( @@ -502,7 +521,7 @@ describe("InlineMenuFieldQualificationService", () => { ).toBe(false); }); - it("is structured on a page with multiple viewable password field", () => { + it("is structured on a page with multiple viewable password fields", () => { const field = mock({ type: "text", autoCompleteType: "", @@ -534,7 +553,7 @@ describe("InlineMenuFieldQualificationService", () => { ).toBe(false); }); - it("is structured on a page with a with no visible password fields and but contains a disabled autocomplete type", () => { + it("contains a disabled autocomplete type when multiple password fields are on the page", () => { const field = mock({ type: "text", autoCompleteType: "off", @@ -552,7 +571,16 @@ describe("InlineMenuFieldQualificationService", () => { form: "validFormId", viewable: false, }); - pageDetails.fields = [field, passwordField]; + const secondPasswordField = mock({ + type: "password", + autoCompleteType: "current-password", + htmlID: "second-password", + htmlName: "second-password", + placeholder: "second-password", + form: "validFormId", + viewable: false, + }); + pageDetails.fields = [field, passwordField, secondPasswordField]; expect( inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails), @@ -713,4 +741,224 @@ describe("InlineMenuFieldQualificationService", () => { }); }); }); + + describe("isFieldForCreditCardForm", () => { + describe("an invalid credit card field", () => { + it("has reference to a `new field` keyword", () => { + const field = mock({ + placeholder: "new credit card", + }); + + expect( + inlineMenuFieldQualificationService.isFieldForCreditCardForm(field, pageDetails), + ).toBe(false); + }); + + describe("does not have a parent form", () => { + it("has no credit card number fields in the page details", () => { + const field = mock({ + placeholder: "name", + }); + const secondField = mock({ + placeholder: "card cvv", + autoCompleteType: "cc-csc", + }); + pageDetails.forms = {}; + pageDetails.fields = [field, secondField]; + + expect( + inlineMenuFieldQualificationService.isFieldForCreditCardForm(field, pageDetails), + ).toBe(false); + }); + + it("has no credit card cvv fields in the page details", () => { + const field = mock({ + placeholder: "name", + }); + const secondField = mock({ + placeholder: "card number", + autoCompleteType: "cc-number", + }); + pageDetails.forms = {}; + pageDetails.fields = [field, secondField]; + + expect( + inlineMenuFieldQualificationService.isFieldForCreditCardForm(field, pageDetails), + ).toBe(false); + }); + }); + + describe("has a parent form", () => { + let form: MockProxy; + + beforeEach(() => { + form = mock({ opid: "validFormId" }); + pageDetails.forms = { + validFormId: form, + }; + }); + + it("does not have a credit card number field within the same form", () => { + const field = mock({ + placeholder: "name", + form: "validFormId", + }); + const cardCvvField = mock({ + placeholder: "card cvv", + autoCompleteType: "cc-csc", + form: "validFormId", + }); + pageDetails.fields = [field, cardCvvField]; + + expect( + inlineMenuFieldQualificationService.isFieldForCreditCardForm(field, pageDetails), + ).toBe(false); + }); + + it("does not contain a cvv field within the same form", () => { + const field = mock({ + placeholder: "name", + form: "validFormId", + }); + const cardNumberField = mock({ + placeholder: "card number", + autoCompleteType: "cc-number", + form: "validFormId", + }); + + pageDetails.fields = [field, cardNumberField]; + + expect( + inlineMenuFieldQualificationService.isFieldForCreditCardForm(field, pageDetails), + ).toBe(false); + }); + }); + }); + + describe("a valid credit card field", () => { + describe("does not have a parent form", () => { + it("is structured on a page with a single credit card number field and a single cvv field", () => { + const field = mock({ + placeholder: "name", + }); + const cardNumberField = mock({ + placeholder: "card number", + autoCompleteType: "cc-number", + }); + const cardCvvField = mock({ + placeholder: "card cvv", + autoCompleteType: "cc-csc", + }); + pageDetails.forms = {}; + pageDetails.fields = [field, cardNumberField, cardCvvField]; + + expect( + inlineMenuFieldQualificationService.isFieldForCreditCardForm(field, pageDetails), + ).toBe(true); + }); + }); + + describe("has a parent form", () => { + let form: MockProxy; + + beforeEach(() => { + form = mock({ opid: "validFormId" }); + pageDetails.forms = { + validFormId: form, + }; + }); + + it("has a credit card number field and cvv field structured within the same form", () => { + const field = mock({ + placeholder: "name", + form: "validFormId", + }); + const cardNumberField = mock({ + placeholder: "card number", + autoCompleteType: "cc-number", + form: "validFormId", + }); + const cardCvvField = mock({ + placeholder: "card cvv", + autoCompleteType: "cc-csc", + form: "validFormId", + }); + pageDetails.fields = [field, cardNumberField, cardCvvField]; + + expect( + inlineMenuFieldQualificationService.isFieldForCreditCardForm(field, pageDetails), + ).toBe(true); + }); + }); + }); + }); + + describe("isFieldForAccountCreationForm", () => { + it("validates a field for an account creation if the field is formless but at least one new password field exists in the page details", () => { + const field = mock({ + placeholder: "username", + autoCompleteType: "username", + type: "text", + htmlName: "username", + htmlID: "username", + }); + const passwordField = mock({ + placeholder: "new password", + autoCompleteType: "new-password", + type: "password", + htmlName: "new-password", + htmlID: "new-password", + }); + pageDetails.forms = {}; + pageDetails.fields = [field, passwordField]; + + expect( + inlineMenuFieldQualificationService.isFieldForAccountCreationForm(field, pageDetails), + ).toBe(true); + }); + + it("validates a field for an account creation if the field is formless and contains an account creation keyword", () => { + const field = mock({ + placeholder: "register username", + autoCompleteType: "username", + type: "text", + htmlName: "username", + htmlID: "username", + }); + pageDetails.forms = {}; + pageDetails.fields = [field]; + + expect( + inlineMenuFieldQualificationService.isFieldForAccountCreationForm(field, pageDetails), + ).toBe(true); + }); + }); + + describe("isFieldForIdentityUsername", () => { + it("returns true if the field contains a keyword indicating that it is for a username field", () => { + const field = mock({ + placeholder: "user-name", + autoCompleteType: "", + type: "text", + htmlName: "user-name", + htmlID: "user-name", + }); + + expect(inlineMenuFieldQualificationService.isFieldForIdentityUsername(field)).toBe(true); + }); + }); + + describe("isEmailField", () => { + it("returns true if the field type is of `email`", () => { + const field = mock({ + placeholder: "email", + autoCompleteType: "", + type: "email", + htmlName: "email", + htmlID: "email", + }); + + expect(inlineMenuFieldQualificationService.isEmailField(field)).toBe(true); + }); + }); }); 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 7bc027b392c..0b04b83ce4e 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 @@ -1,31 +1,131 @@ import AutofillField from "../models/autofill-field"; import AutofillPageDetails from "../models/autofill-page-details"; -import { sendExtensionMessage } from "../utils"; +import { getSubmitButtonKeywordsSet, sendExtensionMessage } from "../utils"; -import { InlineMenuFieldQualificationsService as InlineMenuFieldQualificationsServiceInterface } from "./abstractions/inline-menu-field-qualifications.service"; -import { AutoFillConstants } from "./autofill-constants"; +import { + AutofillKeywordsMap, + InlineMenuFieldQualificationService as InlineMenuFieldQualificationServiceInterface, + SubmitButtonKeywordsMap, +} from "./abstractions/inline-menu-field-qualifications.service"; +import { + AutoFillConstants, + CreditCardAutoFillConstants, + IdentityAutoFillConstants, + SubmitChangePasswordButtonNames, + SubmitLoginButtonNames, +} from "./autofill-constants"; export class InlineMenuFieldQualificationService - implements InlineMenuFieldQualificationsServiceInterface + implements InlineMenuFieldQualificationServiceInterface { private searchFieldNamesSet = new Set(AutoFillConstants.SearchFieldNames); - private excludedAutofillLoginTypesSet = new Set(AutoFillConstants.ExcludedAutofillLoginTypes); + private excludedAutofillFieldTypesSet = new Set(AutoFillConstants.ExcludedAutofillLoginTypes); private usernameFieldTypes = new Set(["text", "email", "number", "tel"]); - private usernameAutocompleteValues = new Set(["username", "email"]); + 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(","); - private currentPasswordAutocompleteValues = new Set(["current-password"]); - private newPasswordAutoCompleteValues = new Set(["new-password"]); - private autofillFieldKeywordsMap: WeakMap = new WeakMap(); + private currentPasswordAutocompleteValue = "current-password"; + private newPasswordAutoCompleteValue = "new-password"; + private autofillFieldKeywordsMap: AutofillKeywordsMap = new WeakMap(); + private submitButtonKeywordsMap: SubmitButtonKeywordsMap = new WeakMap(); private autocompleteDisabledValues = new Set(["off", "false"]); private newFieldKeywords = new Set(["new", "change", "neue", "ändern"]); - private accountCreationFieldKeywords = new Set([ - "register", - "registration", - "create", - "confirm", - ...this.newFieldKeywords, + private accountCreationFieldKeywords = [ + ...new Set(["register", "registration", "create", "confirm", ...this.newFieldKeywords]), + ]; + private creditCardFieldKeywords = [ + ...new Set([ + ...CreditCardAutoFillConstants.CardHolderFieldNames, + ...CreditCardAutoFillConstants.CardNumberFieldNames, + ...CreditCardAutoFillConstants.CardExpiryFieldNames, + ...CreditCardAutoFillConstants.ExpiryMonthFieldNames, + ...CreditCardAutoFillConstants.ExpiryYearFieldNames, + ...CreditCardAutoFillConstants.CVVFieldNames, + ...CreditCardAutoFillConstants.CardBrandFieldNames, + ]), + ]; + private creditCardNameAutocompleteValues = new Set([ + "cc-name", + "cc-given-name,", + "cc-additional-name", + "cc-family-name", ]); + private creditCardExpirationDateAutocompleteValue = "cc-exp"; + private creditCardExpirationMonthAutocompleteValue = "cc-exp-month"; + private creditCardExpirationYearAutocompleteValue = "cc-exp-year"; + private creditCardCvvAutocompleteValue = "cc-csc"; + private creditCardNumberAutocompleteValue = "cc-number"; + private creditCardTypeAutocompleteValue = "cc-type"; + private creditCardAutocompleteValues = new Set([ + ...this.creditCardNameAutocompleteValues, + this.creditCardExpirationDateAutocompleteValue, + this.creditCardExpirationMonthAutocompleteValue, + this.creditCardExpirationYearAutocompleteValue, + this.creditCardNumberAutocompleteValue, + this.creditCardCvvAutocompleteValue, + this.creditCardTypeAutocompleteValue, + ]); + private identityHonorificPrefixAutocompleteValue = "honorific-prefix"; + private identityFullNameAutocompleteValue = "name"; + private identityFirstNameAutocompleteValue = "given-name"; + private identityMiddleNameAutocompleteValue = "additional-name"; + private identityLastNameAutocompleteValue = "family-name"; + private identityNameAutocompleteValues = new Set([ + this.identityFullNameAutocompleteValue, + this.identityHonorificPrefixAutocompleteValue, + this.identityFirstNameAutocompleteValue, + this.identityMiddleNameAutocompleteValue, + this.identityLastNameAutocompleteValue, + "honorific-suffix", + "nickname", + ]); + private identityCompanyAutocompleteValue = "organization"; + private identityStreetAddressAutocompleteValue = "street-address"; + private identityAddressLine1AutocompleteValue = "address-line1"; + private identityAddressLine2AutocompleteValue = "address-line2"; + private identityAddressLine3AutocompleteValue = "address-line3"; + private identityAddressCityAutocompleteValue = "address-level2"; + private identityAddressStateAutocompleteValue = "address-level1"; + private identityAddressAutoCompleteValues = new Set([ + this.identityStreetAddressAutocompleteValue, + this.identityAddressLine1AutocompleteValue, + this.identityAddressLine2AutocompleteValue, + this.identityAddressLine3AutocompleteValue, + this.identityAddressCityAutocompleteValue, + this.identityAddressStateAutocompleteValue, + "shipping", + "billing", + "address-level4", + "address-level3", + ]); + private identityCountryAutocompleteValues = new Set(["country", "country-name"]); + private identityPostalCodeAutocompleteValue = "postal-code"; + private identityPhoneAutocompleteValue = "tel"; + private identityPhoneNumberAutocompleteValues = new Set([ + this.identityPhoneAutocompleteValue, + "tel-country-code", + "tel-area-code", + "tel-local", + "tel-extension", + ]); + private identityAutocompleteValues = new Set([ + ...this.identityNameAutocompleteValues, + ...this.loginUsernameAutocompleteValues, + ...this.identityCompanyAutocompleteValue, + ...this.identityAddressAutoCompleteValues, + ...this.identityCountryAutocompleteValues, + ...this.identityPhoneNumberAutocompleteValues, + this.identityCompanyAutocompleteValue, + this.identityPostalCodeAutocompleteValue, + ]); + private totpFieldAutocompleteValue = "one-time-code"; private inlineMenuFieldQualificationFlagSet = false; constructor() { @@ -46,6 +146,11 @@ export class InlineMenuFieldQualificationService return this.isFieldForLoginFormFallback(field); } + const isTotpField = this.isTotpField(field); + if (isTotpField) { + return false; + } + const isCurrentPasswordField = this.isCurrentPasswordField(field); if (isCurrentPasswordField) { return this.isPasswordFieldForLoginForm(field, pageDetails); @@ -59,6 +164,119 @@ export class InlineMenuFieldQualificationService return this.isUsernameFieldForLoginForm(field, pageDetails); } + /** + * Validates the provided field as a field for a credit card form. + * + * @param field - The field to validate + * @param pageDetails - The details of the page that the field is on. + */ + isFieldForCreditCardForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean { + // If the field contains any of the standardized autocomplete attribute values + // for credit card fields, we should assume that the field is part of a credit card form. + if (this.fieldContainsAutocompleteValues(field, this.creditCardAutocompleteValues)) { + return true; + } + + // If the field contains any keywords indicating this is for a "new" or "changed" credit card + // field, we should assume that the field is not going to be autofilled. + if (this.keywordsFoundInFieldData(field, [...this.newFieldKeywords])) { + return false; + } + + const parentForm = pageDetails.forms[field.form]; + + // If the field does not have a parent form + if (!parentForm) { + // If a credit card number field is not present on the page or there are multiple credit + // card number fields, this field is not part of a credit card form. + const numberFieldsInPageDetails = pageDetails.fields.filter(this.isFieldForCardNumber); + if (numberFieldsInPageDetails.length !== 1) { + return false; + } + + // If a credit card CVV field is not present on the page or there are multiple credit card + // CVV fields, this field is not part of a credit card form. + const cvvFieldsInPageDetails = pageDetails.fields.filter(this.isFieldForCardCvv); + if (cvvFieldsInPageDetails.length !== 1) { + return false; + } + + return this.keywordsFoundInFieldData(field, this.creditCardFieldKeywords); + } + + // If the field has a parent form, check the fields from that form exclusively + const fieldsFromSameForm = pageDetails.fields.filter((f) => f.form === field.form); + + // If a credit card number field is not present on the page or there are multiple credit + // card number fields, this field is not part of a credit card form. + const numberFieldsInPageDetails = fieldsFromSameForm.filter(this.isFieldForCardNumber); + if (numberFieldsInPageDetails.length !== 1) { + return false; + } + + // If a credit card CVV field is not present on the page or there are multiple credit card + // CVV fields, this field is not part of a credit card form. + const cvvFieldsInPageDetails = fieldsFromSameForm.filter(this.isFieldForCardCvv); + if (cvvFieldsInPageDetails.length !== 1) { + return false; + } + + return this.keywordsFoundInFieldData(field, [...this.creditCardFieldKeywords]); + } + + /** Validates the provided field as a field for an account creation form. + * + * @param field - The field to validate + * @param pageDetails - The details of the page that the field is on. + */ + isFieldForAccountCreationForm(field: AutofillField, pageDetails: AutofillPageDetails): boolean { + if (this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet)) { + return false; + } + + if (!this.isUsernameField(field) && !this.isPasswordField(field)) { + return false; + } + + const parentForm = pageDetails.forms[field.form]; + + if (!parentForm) { + // If the field does not have a parent form, but we can identify that the page contains at least + // one new password field, we should assume that the field is part of an account creation form. + const newPasswordFields = pageDetails.fields.filter(this.isNewPasswordField); + if (newPasswordFields.length >= 1) { + return true; + } + + // If no password fields are found on the page, check for keywords that indicate the field is + // part of an account creation form. + return this.keywordsFoundInFieldData(field, this.accountCreationFieldKeywords); + } + + // If the field has a parent form, check the fields from that form exclusively + const fieldsFromSameForm = pageDetails.fields.filter((f) => f.form === field.form); + const newPasswordFields = fieldsFromSameForm.filter(this.isNewPasswordField); + if (newPasswordFields.length >= 1) { + return true; + } + + return this.keywordsFoundInFieldData(field, this.accountCreationFieldKeywords); + } + + /** + * Validates the provided field as a field for an identity form. + * + * @param field - The field to validate + * @param _pageDetails - Currently unused, will likely be required in the future + */ + isFieldForIdentityForm(field: AutofillField, _pageDetails: AutofillPageDetails): boolean { + if (this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet)) { + return false; + } + + return this.fieldContainsAutocompleteValues(field, this.identityAutocompleteValues); + } + /** * Validates the provided field as a password field for a login form. * @@ -71,12 +289,7 @@ export class InlineMenuFieldQualificationService ): boolean { // If the provided field is set with an autocomplete value of "current-password", we should assume that // the page developer intends for this field to be interpreted as a password field for a login form. - if ( - this.fieldContainsAutocompleteValues( - field.autoCompleteType, - this.currentPasswordAutocompleteValues, - ) - ) { + if (this.fieldContainsAutocompleteValues(field, this.currentPasswordAutocompleteValue)) { return true; } @@ -110,10 +323,7 @@ export class InlineMenuFieldQualificationService // provided field is for a login form. This will only be the case if the field does not // explicitly have its autocomplete attribute set to "off" or "false". - return !this.fieldContainsAutocompleteValues( - field.autoCompleteType, - this.autocompleteDisabledValues, - ); + return !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues); } // If the field has a form parent and there are multiple visible password fields @@ -135,10 +345,7 @@ export class InlineMenuFieldQualificationService // If the field has a form parent and no username field exists and the field has an // autocomplete attribute set to "off" or "false", this is not a password field - return !this.fieldContainsAutocompleteValues( - field.autoCompleteType, - this.autocompleteDisabledValues, - ); + return !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues); } /** @@ -153,10 +360,10 @@ export class InlineMenuFieldQualificationService ): boolean { // If the provided field is set with an autocomplete of "username", we should assume that // the page developer intends for this field to be interpreted as a username field. - if ( - this.fieldContainsAutocompleteValues(field.autoCompleteType, this.usernameAutocompleteValues) - ) { - const newPasswordFieldsInPageDetails = pageDetails.fields.filter(this.isNewPasswordField); + if (this.fieldContainsAutocompleteValues(field, this.loginUsernameAutocompleteValues)) { + const newPasswordFieldsInPageDetails = pageDetails.fields.filter( + (field) => field.viewable && this.isNewPasswordField(field), + ); return newPasswordFieldsInPageDetails.length === 0; } @@ -198,10 +405,7 @@ export class InlineMenuFieldQualificationService // If the page does not contain any password fields, it might be part of a multistep login form. // That will only be the case if the field does not explicitly have its autocomplete attribute // set to "off" or "false". - return !this.fieldContainsAutocompleteValues( - field.autoCompleteType, - this.autocompleteDisabledValues, - ); + return !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues); } // If the field is structured within a form, but no password fields are present in the form, @@ -209,19 +413,14 @@ export class InlineMenuFieldQualificationService if (passwordFieldsInPageDetails.length === 0) { // If the field's autocomplete is set to a disabled value, we should assume that the field is // not part of a login form. - if ( - this.fieldContainsAutocompleteValues( - field.autoCompleteType, - this.autocompleteDisabledValues, - ) - ) { + if (this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues)) { return false; } // If the form that contains the field has more than one visible field, we should assume // that the field is part of an account creation form. const fieldsWithinForm = pageDetails.fields.filter( - (pageDetailsField) => pageDetailsField.form === field.form && pageDetailsField.viewable, + (pageDetailsField) => pageDetailsField.form === field.form, ); return fieldsWithinForm.length === 1; } @@ -241,23 +440,382 @@ export class InlineMenuFieldQualificationService return false; } + // If no visible fields are found on the page, but we have a single password + // field we should assume that the field is part of a login form. + if (passwordFieldsInPageDetails.length === 1) { + return true; + } + // If no visible password fields are found, this field might be part of a multipart form. // Check for an invalid autocompleteType to determine if the field is part of a login form. - return !this.fieldContainsAutocompleteValues( - field.autoCompleteType, - this.autocompleteDisabledValues, - ); + return !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues); } + /** + * Validates the provided field as a credit card name field. + * + * @param field - The field to validate + */ + isFieldForCardholderName = (field: AutofillField): boolean => { + if (this.fieldContainsAutocompleteValues(field, this.creditCardNameAutocompleteValues)) { + return true; + } + + return this.keywordsFoundInFieldData( + field, + CreditCardAutoFillConstants.CardHolderFieldNames, + false, + ); + }; + + /** + * Validates the provided field as a credit card number field. + * + * @param field - The field to validate + */ + isFieldForCardNumber = (field: AutofillField): boolean => { + if (this.fieldContainsAutocompleteValues(field, this.creditCardNumberAutocompleteValue)) { + return true; + } + + return this.keywordsFoundInFieldData( + field, + CreditCardAutoFillConstants.CardNumberFieldNames, + false, + ); + }; + + /** + * Validates the provided field as a credit card expiration date field. + * + * @param field - The field to validate + */ + isFieldForCardExpirationDate = (field: AutofillField): boolean => { + if ( + this.fieldContainsAutocompleteValues(field, this.creditCardExpirationDateAutocompleteValue) + ) { + return true; + } + + return this.keywordsFoundInFieldData( + field, + CreditCardAutoFillConstants.CardExpiryFieldNames, + false, + ); + }; + + /** + * Validates the provided field as a credit card expiration month field. + * + * @param field - The field to validate + */ + isFieldForCardExpirationMonth = (field: AutofillField): boolean => { + if ( + this.fieldContainsAutocompleteValues(field, this.creditCardExpirationMonthAutocompleteValue) + ) { + return true; + } + + return this.keywordsFoundInFieldData( + field, + CreditCardAutoFillConstants.ExpiryMonthFieldNames, + false, + ); + }; + + /** + * Validates the provided field as a credit card expiration year field. + * + * @param field - The field to validate + */ + isFieldForCardExpirationYear = (field: AutofillField): boolean => { + if ( + this.fieldContainsAutocompleteValues(field, this.creditCardExpirationYearAutocompleteValue) + ) { + return true; + } + + return this.keywordsFoundInFieldData( + field, + CreditCardAutoFillConstants.ExpiryYearFieldNames, + false, + ); + }; + + /** + * Validates the provided field as a credit card CVV field. + * + * @param field - The field to validate + */ + isFieldForCardCvv = (field: AutofillField): boolean => { + if (this.fieldContainsAutocompleteValues(field, this.creditCardCvvAutocompleteValue)) { + return true; + } + + return this.keywordsFoundInFieldData(field, CreditCardAutoFillConstants.CVVFieldNames, false); + }; + + /** + * Validates the provided field as an identity title type field. + * + * @param field - The field to validate + */ + isFieldForIdentityTitle = (field: AutofillField): boolean => { + if ( + this.fieldContainsAutocompleteValues(field, this.identityHonorificPrefixAutocompleteValue) + ) { + return true; + } + + return this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.TitleFieldNames, false); + }; + + /** + * Validates the provided field as an identity full name field. + * + * @param field + */ + isFieldForIdentityFirstName = (field: AutofillField): boolean => { + if (this.fieldContainsAutocompleteValues(field, this.identityFirstNameAutocompleteValue)) { + return true; + } + + return this.keywordsFoundInFieldData( + field, + IdentityAutoFillConstants.FirstnameFieldNames, + false, + ); + }; + + /** + * Validates the provided field as an identity middle name field. + * + * @param field - The field to validate + */ + isFieldForIdentityMiddleName = (field: AutofillField): boolean => { + if (this.fieldContainsAutocompleteValues(field, this.identityMiddleNameAutocompleteValue)) { + return true; + } + + return this.keywordsFoundInFieldData( + field, + IdentityAutoFillConstants.MiddlenameFieldNames, + false, + ); + }; + + /** + * Validates the provided field as an identity last name field. + * + * @param field - The field to validate + */ + isFieldForIdentityLastName = (field: AutofillField): boolean => { + if (this.fieldContainsAutocompleteValues(field, this.identityLastNameAutocompleteValue)) { + return true; + } + + return this.keywordsFoundInFieldData( + field, + IdentityAutoFillConstants.LastnameFieldNames, + false, + ); + }; + + /** + * Validates the provided field as an identity full name field. + * + * @param field - The field to validate + */ + isFieldForIdentityFullName = (field: AutofillField): boolean => { + if (this.fieldContainsAutocompleteValues(field, this.identityFullNameAutocompleteValue)) { + return true; + } + + return this.keywordsFoundInFieldData( + field, + IdentityAutoFillConstants.FullNameFieldNames, + false, + ); + }; + + /** + * Validates the provided field as an identity address field. + * + * @param field - The field to validate + */ + isFieldForIdentityAddress1 = (field: AutofillField): boolean => { + if (this.fieldContainsAutocompleteValues(field, this.identityAddressLine1AutocompleteValue)) { + return true; + } + + return this.keywordsFoundInFieldData( + field, + [ + ...IdentityAutoFillConstants.AddressFieldNames, + ...IdentityAutoFillConstants.Address1FieldNames, + ], + false, + ); + }; + + /** + * Validates the provided field as an identity address field. + * + * @param field - The field to validate + */ + isFieldForIdentityAddress2 = (field: AutofillField): boolean => { + if (this.fieldContainsAutocompleteValues(field, this.identityAddressLine2AutocompleteValue)) { + return true; + } + + return this.keywordsFoundInFieldData( + field, + IdentityAutoFillConstants.Address2FieldNames, + false, + ); + }; + + /** + * Validates the provided field as an identity address field. + * + * @param field - The field to validate + */ + isFieldForIdentityAddress3 = (field: AutofillField): boolean => { + if (this.fieldContainsAutocompleteValues(field, this.identityAddressLine3AutocompleteValue)) { + return true; + } + + return this.keywordsFoundInFieldData( + field, + IdentityAutoFillConstants.Address3FieldNames, + false, + ); + }; + + /** + * Validates the provided field as an identity city field. + * + * @param field - The field to validate + */ + isFieldForIdentityCity = (field: AutofillField): boolean => { + if (this.fieldContainsAutocompleteValues(field, this.identityAddressCityAutocompleteValue)) { + return true; + } + + return this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.CityFieldNames, false); + }; + + /** + * Validates the provided field as an identity state field. + * + * @param field - The field to validate + */ + isFieldForIdentityState = (field: AutofillField): boolean => { + if (this.fieldContainsAutocompleteValues(field, this.identityAddressStateAutocompleteValue)) { + return true; + } + + return this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.StateFieldNames, false); + }; + + /** + * Validates the provided field as an identity postal code field. + * + * @param field - The field to validate + */ + isFieldForIdentityPostalCode = (field: AutofillField): boolean => { + if (this.fieldContainsAutocompleteValues(field, this.identityPostalCodeAutocompleteValue)) { + return true; + } + + return this.keywordsFoundInFieldData( + field, + IdentityAutoFillConstants.PostalCodeFieldNames, + false, + ); + }; + + /** + * Validates the provided field as an identity country field. + * + * @param field - The field to validate + */ + isFieldForIdentityCountry = (field: AutofillField): boolean => { + if (this.fieldContainsAutocompleteValues(field, this.identityCountryAutocompleteValues)) { + return true; + } + + return this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.CountryFieldNames, false); + }; + + /** + * Validates the provided field as an identity company field. + * + * @param field - The field to validate + */ + isFieldForIdentityCompany = (field: AutofillField): boolean => { + if (this.fieldContainsAutocompleteValues(field, this.identityCompanyAutocompleteValue)) { + return true; + } + + return this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.CompanyFieldNames, false); + }; + + /** + * Validates the provided field as an identity phone field. + * + * @param field - The field to validate + */ + isFieldForIdentityPhone = (field: AutofillField): boolean => { + if (this.fieldContainsAutocompleteValues(field, this.identityPhoneAutocompleteValue)) { + return true; + } + + return this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.PhoneFieldNames, false); + }; + + /** + * Validates the provided field as an identity email field. + * + * @param field - The field to validate + */ + isFieldForIdentityEmail = (field: AutofillField): boolean => { + if ( + this.fieldContainsAutocompleteValues(field, this.emailAutocompleteValue) || + field.type === "email" + ) { + return true; + } + + return this.keywordsFoundInFieldData(field, IdentityAutoFillConstants.EmailFieldNames, false); + }; + + /** + * Validates the provided field as an identity username field. + * + * @param field - The field to validate + */ + isFieldForIdentityUsername = (field: AutofillField): boolean => { + if (this.fieldContainsAutocompleteValues(field, this.usernameAutocompleteValue)) { + return true; + } + + return this.keywordsFoundInFieldData( + field, + IdentityAutoFillConstants.UserNameFieldNames, + false, + ); + }; + /** * Validates the provided field as a username field. * * @param field - The field to validate */ - private isUsernameField = (field: AutofillField): boolean => { + isUsernameField = (field: AutofillField): boolean => { if ( !this.usernameFieldTypes.has(field.type) || - this.isExcludedFieldType(field, this.excludedAutofillLoginTypesSet) + this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet) ) { return false; } @@ -265,18 +823,31 @@ export class InlineMenuFieldQualificationService return this.keywordsFoundInFieldData(field, AutoFillConstants.UsernameFieldNames); }; + /** + * Validates the provided field as an email field. + * + * @param field - The field to validate + */ + isEmailField = (field: AutofillField): boolean => { + if (field.type === "email") { + return true; + } + + return ( + !this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet) && + this.keywordsFoundInFieldData(field, AutoFillConstants.EmailFieldNames) + ); + }; + /** * Validates the provided field as a current password field. * * @param field - The field to validate */ - private isCurrentPasswordField = (field: AutofillField): boolean => { + isCurrentPasswordField = (field: AutofillField): boolean => { if ( - this.fieldContainsAutocompleteValues( - field.autoCompleteType, - this.newPasswordAutoCompleteValues, - ) || - this.keywordsFoundInFieldData(field, [...this.accountCreationFieldKeywords]) + this.fieldContainsAutocompleteValues(field, this.newPasswordAutoCompleteValue) || + this.keywordsFoundInFieldData(field, this.accountCreationFieldKeywords) ) { return false; } @@ -289,19 +860,14 @@ export class InlineMenuFieldQualificationService * * @param field - The field to validate */ - private isNewPasswordField = (field: AutofillField): boolean => { - if ( - this.fieldContainsAutocompleteValues( - field.autoCompleteType, - this.currentPasswordAutocompleteValues, - ) - ) { + isNewPasswordField = (field: AutofillField): boolean => { + if (this.fieldContainsAutocompleteValues(field, this.currentPasswordAutocompleteValue)) { return false; } return ( this.isPasswordField(field) && - this.keywordsFoundInFieldData(field, [...this.accountCreationFieldKeywords]) + this.keywordsFoundInFieldData(field, this.accountCreationFieldKeywords) ); }; @@ -314,8 +880,9 @@ export class InlineMenuFieldQualificationService const isInputPasswordType = field.type === "password"; if ( (!isInputPasswordType && - this.isExcludedFieldType(field, this.excludedAutofillLoginTypesSet)) || - this.fieldHasDisqualifyingAttributeValue(field) + this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet)) || + this.fieldHasDisqualifyingAttributeValue(field) || + this.isTotpField(field) ) { return false; } @@ -363,6 +930,22 @@ export class InlineMenuFieldQualificationService return !(this.passwordFieldExcludeListString.indexOf(cleanedValue) > -1); } + /** + * Validates whether the provided field is a TOTP field. + * + * @param field - The field to validate + */ + private isTotpField = (field: AutofillField): boolean => { + if (this.fieldContainsAutocompleteValues(field, this.totpFieldAutocompleteValue)) { + return true; + } + + return ( + !this.isExcludedFieldType(field, this.excludedAutofillFieldTypesSet) && + this.keywordsFoundInFieldData(field, AutoFillConstants.TotpFieldNames) + ); + }; + /** * Validates the provided field to indicate if the field has a * disqualifying attribute that would impede autofill entirely. @@ -427,65 +1010,129 @@ export class InlineMenuFieldQualificationService return false; } + /** + * Validates the provided field to indicate if the field is a submit button for a login form. + * + * @param element - The element to validate + */ + isElementLoginSubmitButton = (element: HTMLElement): boolean => { + const keywordValues = this.getSubmitButtonKeywords(element); + return SubmitLoginButtonNames.some((keyword) => keywordValues.indexOf(keyword) > -1); + }; + + /** + * Validates the provided field to indicate if the field is a submit button for a change password form. + * + * @param element - The element to validate + */ + isElementChangePasswordSubmitButton = (element: HTMLElement): boolean => { + const keywordValues = this.getSubmitButtonKeywords(element); + return SubmitChangePasswordButtonNames.some((keyword) => keywordValues.indexOf(keyword) > -1); + }; + + /** + * Gather the keywords from the provided element to validate the submit button. + * + * @param element - The element to validate + */ + private getSubmitButtonKeywords(element: HTMLElement): string { + if (!this.submitButtonKeywordsMap.has(element)) { + const keywordsSet = getSubmitButtonKeywordsSet(element); + this.submitButtonKeywordsMap.set(element, Array.from(keywordsSet).join(",")); + } + + return this.submitButtonKeywordsMap.get(element); + } + /** * Validates the provided field to indicate if the field has any of the provided keywords. * * @param autofillFieldData - The field data to search for keywords * @param keywords - The keywords to search for + * @param fuzzyMatchKeywords - Indicates if the keywords should be matched in a fuzzy manner */ - private keywordsFoundInFieldData(autofillFieldData: AutofillField, keywords: string[]) { - const searchedString = this.getAutofillFieldDataKeywords(autofillFieldData); - return keywords.some((keyword) => searchedString.includes(keyword)); + private keywordsFoundInFieldData( + autofillFieldData: AutofillField, + keywords: string[], + fuzzyMatchKeywords = true, + ) { + const searchedValues = this.getAutofillFieldDataKeywords(autofillFieldData, fuzzyMatchKeywords); + const parsedKeywords = keywords.map((keyword) => keyword.replace(/-/g, "")); + + if (typeof searchedValues === "string") { + return parsedKeywords.some((keyword) => searchedValues.indexOf(keyword) > -1); + } + + return parsedKeywords.some((keyword) => searchedValues.has(keyword)); } /** * Retrieves the keywords from the provided autofill field data. * * @param autofillFieldData - The field data to search for keywords + * @param returnStringValue - Indicates if the method should return a string value */ - private getAutofillFieldDataKeywords(autofillFieldData: AutofillField) { - if (this.autofillFieldKeywordsMap.has(autofillFieldData)) { - return this.autofillFieldKeywordsMap.get(autofillFieldData); + private getAutofillFieldDataKeywords( + autofillFieldData: AutofillField, + returnStringValue: boolean, + ) { + if (!this.autofillFieldKeywordsMap.has(autofillFieldData)) { + const keywords = [ + autofillFieldData.htmlID, + autofillFieldData.htmlName, + autofillFieldData.htmlClass, + autofillFieldData.type, + autofillFieldData.title, + autofillFieldData.placeholder, + autofillFieldData.autoCompleteType, + autofillFieldData["label-data"], + autofillFieldData["label-aria"], + autofillFieldData["label-left"], + autofillFieldData["label-right"], + autofillFieldData["label-tag"], + autofillFieldData["label-top"], + ]; + const keywordsSet = new Set(); + for (let i = 0; i < keywords.length; i++) { + if (typeof keywords[i] === "string") { + keywords[i] + .toLowerCase() + .replace(/-/g, "") + .replace(/[^a-zA-Z0-9]+/g, "|") + .split("|") + .forEach((keyword) => keywordsSet.add(keyword)); + } + } + + const stringValue = Array.from(keywordsSet).join(","); + this.autofillFieldKeywordsMap.set(autofillFieldData, { keywordsSet, stringValue }); } - const keywordValues = [ - autofillFieldData.htmlID, - autofillFieldData.htmlName, - autofillFieldData.htmlClass, - autofillFieldData.type, - autofillFieldData.title, - autofillFieldData.placeholder, - autofillFieldData.autoCompleteType, - autofillFieldData["label-data"], - autofillFieldData["label-aria"], - autofillFieldData["label-left"], - autofillFieldData["label-right"], - autofillFieldData["label-tag"], - autofillFieldData["label-top"], - ] - .join(",") - .toLowerCase(); - this.autofillFieldKeywordsMap.set(autofillFieldData, keywordValues); - - return keywordValues; + const mapValues = this.autofillFieldKeywordsMap.get(autofillFieldData); + return returnStringValue ? mapValues.stringValue : mapValues.keywordsSet; } /** * Separates the provided field data into space-separated values and checks if any * of the values are present in the provided set of autocomplete values. * - * @param fieldAutocompleteValue - The field autocomplete value to validate + * @param autofillFieldData - The field autocomplete value to validate * @param compareValues - The set of autocomplete values to check against */ private fieldContainsAutocompleteValues( - fieldAutocompleteValue: string, - compareValues: Set, + autofillFieldData: AutofillField, + compareValues: string | Set, ) { - if (!fieldAutocompleteValue) { + const fieldAutocompleteValue = autofillFieldData.autoCompleteType; + if (!fieldAutocompleteValue || typeof fieldAutocompleteValue !== "string") { return false; } const autocompleteValueParts = fieldAutocompleteValue.split(" "); + if (typeof compareValues === "string") { + return autocompleteValueParts.indexOf(compareValues) > -1; + } + for (let index = 0; index < autocompleteValueParts.length; index++) { if (compareValues.has(autocompleteValueParts[index])) { return true; 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 6ee5171e58c..e5e21c4b021 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 @@ -1,12 +1,16 @@ +import { mock } from "jest-mock-extended"; + import { EVENTS } from "@bitwarden/common/autofill/constants"; import AutofillScript, { FillScript, FillScriptActions } from "../models/autofill-script"; import { mockQuerySelectorAllDefinedCall } from "../spec/testing-utils"; import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types"; -import AutofillOverlayContentService from "./autofill-overlay-content.service"; -import CollectAutofillContentService from "./collect-autofill-content.service"; +import { InlineMenuFieldQualificationService } from "./abstractions/inline-menu-field-qualifications.service"; +import { AutofillOverlayContentService } from "./autofill-overlay-content.service"; +import { CollectAutofillContentService } from "./collect-autofill-content.service"; import DomElementVisibilityService from "./dom-element-visibility.service"; +import { DomQueryService } from "./dom-query.service"; import InsertAutofillContentService from "./insert-autofill-content.service"; const mockLoginForm = ` @@ -64,10 +68,16 @@ function setMockWindowLocation({ } describe("InsertAutofillContentService", () => { + const inlineMenuFieldQualificationService = mock(); + const domQueryService = new DomQueryService(); const domElementVisibilityService = new DomElementVisibilityService(); - const autofillOverlayContentService = new AutofillOverlayContentService(); + const autofillOverlayContentService = new AutofillOverlayContentService( + domQueryService, + inlineMenuFieldQualificationService, + ); const collectAutofillContentService = new CollectAutofillContentService( domElementVisibilityService, + domQueryService, autofillOverlayContentService, ); let insertAutofillContentService: InsertAutofillContentService; 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..058ce087c61 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.ts @@ -10,12 +10,10 @@ 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 { - private readonly domElementVisibilityService: DomElementVisibilityService; - private readonly collectAutofillContentService: CollectAutofillContentService; private readonly autofillInsertActions: AutofillInsertActions = { fill_by_opid: ({ opid, value }) => this.handleFillFieldByOpidAction(opid, value), click_on_opid: ({ opid }) => this.handleClickOnFieldByOpidAction(opid), @@ -27,12 +25,9 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf * DomElementVisibilityService and CollectAutofillContentService classes. */ constructor( - domElementVisibilityService: DomElementVisibilityService, - collectAutofillContentService: CollectAutofillContentService, - ) { - this.domElementVisibilityService = domElementVisibilityService; - this.collectAutofillContentService = collectAutofillContentService; - } + private domElementVisibilityService: DomElementVisibilityService, + private collectAutofillContentService: CollectAutofillContentService, + ) {} /** * Handles autofill of the forms on the current page based on the diff --git a/apps/browser/src/autofill/shared/styles/webfonts.scss b/apps/browser/src/autofill/shared/styles/webfonts.scss new file mode 100644 index 00000000000..d0bc054ff88 --- /dev/null +++ b/apps/browser/src/autofill/shared/styles/webfonts.scss @@ -0,0 +1,416 @@ +/* cyrillic-ext */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAAAQMAA8AAAAAB5AAAAO0AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiYbgTYcOgZgP1NUQVReAFQREAqBbIFxCxAAATYCJAMcBCAFhHgHSBt+Bgi+DJhjiLKs+yJRTbogxxKbIXMfd0QzZTgfG/4m7I5QyXmefr9nv/a59hCX5N7MGqWZZbHGdNHMEC2qJIYS8P99yO7zzZ5RaY5w9DELzTJiB9bp4s6oXDRxnS6deHj+Xj33Z04PxhOKT4tHx2qL3YItLpKSLbFqXmPkfLHLUtNxxzgbJx/MRlr0azc4z1cfBD8efNCGcbETQdU21zRR+5+znr1PfyonGzyahyn2vFeyiuxhYyywKVbCNWjTv2LPa90gBbZlO7OT9i1J2kwmY7kBAS9eFOMBDBRBT0QrGkVUgr6WWRQ1HZ1eBt3sc3TQg8M00CMLOqDXAc8EFQn/IyBKKonMG40fDMmIIChmitYajOiL2GYWsUGiqCU719aJQ/REjZ6YLRsxPn1k/mDUAyEaQSvZ7sjxHubruXbtihOgAYXtdf58zivQjPOfwHVrL1q1MbsIausg8Ee2rubGyCuY9Bd75t/0LsIp8gn1MuoBJosU0VNhuAjMJ9yHPZkQDh9kj0BOXkAnOQQzZYtWKUpMAASVKFHnMuULQYmxwoBCiSsgjbQO9NdT1Bhg8mAm/3tvm/uSSLr38OdsEAAQlZ6+BugSKYgODESNNeAQGAz6A4CSELqthKHPR/XuXd+THz58ybGv3/ud+vb5yNF5j+LHrznxJZ7x7tNxcvfj/gTcS4urlvVk/0if9Lmjb1E4+nrv1pu9vZJ5I780fPWurJu7tlD99J9nP7aPv7pHSsrnl5vUNSnlybh7//d9RU3ErNeO0Vouc/ih2csm5J1HUnmC7MstrQN1VZfE79fZhWP9UaiHImMV6LSXC6lIIGn4E/WpiqDyrPfXrfJSuv8l9MDnj49/AN8OfMmlrdF1oFVA+Iim7hbEyVQfBLGqyz/3GqD0dbqdOERS/kaswTpTCNaIPEFWBAM9kJN8H9yLPA8xNBJRDPQfKku8RW2qa2iMdRqtyXR5oEXaFaG/+Yi+pqI20H8YK9FIajzX97wTxGMXzhBgmuAJ0ClCZnEOQ4BujSLMI1vDKCQyT1+nx9KpTh1XiENh8bhquRS6WpxDUmfOqGlzLIxpSYDJVWMRI/HpAhyrGAeHOBPVpFa9ek3so0MTte28nS5j5swZ01Xc6fYqax3CWfZx+leoRvV6eGqZjKFXnj7PwamYEA81wOeRcQ4XVf5IjzsjEiUZ8gVRCeGMkJTjJDpGxJk8rjq82uNypoJA6Pw/jKztSJZKZUFEpql34auEocUY31QaRpuHbXB4BA==) + format("woff2"); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* hebrew */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F; +} +/* math */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0330, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, + U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2034-2037, U+2057, U+20D0-20DC, U+20E1, + U+20E5-20EF, U+2102, U+210A-210E, U+2110-2112, U+2115, U+2119-211D, U+2124, U+2128, U+212C-212D, + U+212F-2131, U+2133-2138, U+213C-2140, U+2145-2149, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, + U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, + U+2336-237A, U+237C, U+2395, U+239B-23B6, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, + U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, + U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF; +} +/* symbols */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, + U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, + U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, + U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, + U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, + U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, + U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, + U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, + U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, + U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, + U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, + U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, + U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, + U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, + U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8B1, + U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, + U+1FA80-1FA88, U+1FA90-1FABD, U+1FABF-1FAC5, U+1FACE-1FADB, U+1FAE0-1FAE8, U+1FAF0-1FAF8, + U+1FB00-1FBFF; +} +/* vietnamese */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAABQIAA8AAAAAK/QAABOpAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGmobi2wcgiYGYD9TVEFUXgCBOBEQCqw4p00LgiYAATYCJAOESAQgBYR4B4pUG1wnRQdq2DgABtfGIfv/cMANGAI11H4HdagP6go7obWvdlXpMZrlxGKrG02Yxg8fEUouGb7tYEcxEEb8oTrC4NpAm5e0IqD5z4NL5N/Z+L42rzb+P+FLhKe8l/88zdmf+2QmzCTBZGmYtkFqDjWl4lhFLDVKKlkxqJDdb0JNPVLxrIjAzNFam31MIpWNiPu5/akiFho1Q0QtW9/+fHARhUKhUFA+J2Fz1ChFeYRFYu3yDJmweWgSFTYLZErK6y8b8y6l+QZ+R9NLcR+Cm90z/DtMquY21+TgSW1uTW5iY9OjQWz7/7andHQzLlrr16KIQ7zfmEjqIWzOgSEkwVC0PP3l9r2u8kI9PfUNaOdqtXNIBgry2TOGCnInieb/oa8vLbCsI5ZpjnF2DRhdZKQUiSp37MhBiGHkMHRo9muv5rxdSZd0N5lkOEbGCCPEYbwqqe3rJn+IIExTAAA4EQZIB9gRxfsqrLTKamsQUomJkigFSZOO1JaF5BgIGYyCDPULed/7uAqVOD8/QgBNrgJDaxtPLODT9yoBfFm5MeAr164A31lNBpgAwLIUAGFXMx9m7uNmK5gGMGT99gDcJmVGKWIQDSElREhHdoqGshjSJHCiibmrCLUnQ7VchlD9Mjcb1dR2452KHwrIeaY5mqUZmh5ySohqMoJOF0EnyVUvUF1ndvEciD/oKA9tc/hQX8Pci6qXcthNTJ90T0e5mmsoiyFNAqewCcarkfJf+Hf8C/6E3+M3+CV+hh/jUi7hs3ycD/Ju3spbhDOvFERergrfouVUSh6aqSb7Epf8qKACGkD51NVo3yH6+GSE7cOb86a8PndzF0/hcdzOFRhIsz9OyH6ib+gzekVPaqGQNLvzMOyaQLIL7CC/xxbZdHfFDtbPOt3qX1zYRKupgt6gHLXIlxLXn84mstGsBDSE+lFP6vxG27KWrDHL4ZeYOj/GFFmSsLEYXs00Jsik31SGfqCv6BN68fQe0RU6RydojVZonj+hSf6Btinqpf+9kr3UACLNXAsiJvJBjORTEOug9tW24jcj0tHSwVybkYf0ZvibbVme3MSTvWBBErRZwIx0Ax1TrRuw0Rq9IAF/pUvi6U8ReFoZmzM4gz2cwR7OYA97OIMzoumKoVtc6uqsGpO1jbeVX7m3ucJW75I/NoCOGZHpTaOblLaFyVcya79HbJvNM3vYbB71EJp7myskdA9xeV48DnCQP54E2MyILJt6FCFgs3lRnqohftsWoFkxfWzlHCNMQ+bCefw25/HbnEdvC7DffCzT6LyUXMlvO8WAXsD6R9ZfQU+EP82W5a4sdy3CYROxdszaXUAr5yEoiSGLDz6FkCBLWjcxG1d6YbSwkwDnMwg+K1uSk7dPqVLkmxTqUT82M1iwylO8rXc+HjsTjLxzXQHaA1oDmgLqA9yIC9UqADQx5UOotWs5f8mptDXuYR/tGZWP86NWusAddxxBf9TW1OUj8g7k1XMWa7ytnf3zu2ifeGLn4Jx3Tojnyr+phqROEQRQIS9kmwPy9DQtYIN4rnBjDQxgNiM4TUaSxMbGmlTSzwnFN3bTmoi0N6YlaIhC9rGeOdUJ4gJ3/3n0uQw1mE1UCQlywU3Av1SN7S/avHyAzUUN6J/jq0evQ+IWnQHjB7qqZhCw/bMKgL4BQGy1D/VfXSDEBp3QPpuG2OdZEyACFl8GDQLoH0N/RJQekyWJgmNYU6t8RWazoMoM+3UI4rQmOpmVImtSjrBBuxO/XwJgUg4YxCmW6tE0WkuTfB0XPIrXdX3v+tH1u7HdshCPh2AUQzk0lapoggO+UTbMqHL94PrpNwHr6/+IFW+p5vfmfrPYTK+pqBECNXEv33vY5GXZ/SAIcDuBeA3eXyf1NmhwQL1b7QEgCJ0ht5H7ZPx/gFwAyBsAHwmuzq5sy2/ltmrpZtZGKomKl9BV/L6SxeeDUns0Akr6IW26XcRFJ0QJh0tPsQm7fpopLrvdEe00HPEiNq1W9GvRRt2keI2nJcWnOvW3J8mZKuzxw50lmldfMn34v0KhtP8EAiklsd8bGyeLhlNE66ffYa/vQSCgCUOhR1T4OKJXvXxY6nsY1KvCj6n4kSYMPPAqvkBAq/6EILria96jMEEWz9BLhSbL9CUxYr07g8EO0I6FQqH65K0IQmhF2pkBN6DCyIRiej2k7yVQUBQOTysIBDCA6LUyCDiD9/qK4Rk9Dhd6dUSHNs/Ga9OiRlTche+OJrxHhfe9vkAPUUfhsB7YXeqramHTD8U+t9c25pxNusjzsTZ59VF7g8Fa+vpw8Ri9dICFWbTxomN5i2Lv67efBTcTNE7QkRdb+s3P1Wg3vyNNvBUvH8L34IW2VWqky3GacMDO9FWf8sJQaFb5I178+ME8UiP9XyFaZycH6dChVcaeRUUVYSpeFF2WadNboSF3Av00Oj1wAid5A68XFulr72ac3K/rXR++QvHEM7TuEAWe7x1jrqJg4WVM3ZIauFKwy2kLfP1OouJPG1yX9a5uXA/CZ0R9BmKyA6/1jJTbW3zjY/yObF9VK3CJ4YrVzV9w0+bLelWwrExfv/b6ovPhFjrO1O1lJ6py1IqOt6Ir6XuDXVB2mx5qSkUQvkD3Aq3o8nUqvEbXeiCB/n64V1D1AIvnZ58xNBGNq3PO1y2TuLTk4mLsmN8DnA+3OOmPH12s5vNpTBQMTz6r9J3dZ2rEyIlDi9LnLmStL+q3f9T+23323d6h4YscXdmnUbrSEjl8hi3PVvAW7kWzUM4ZY2fAtxXIQ/iWkpvBTcFKJeCEoYb3/48B/7W7Q57IwWM/+X3Df/j9xzmooQeNbL3lxze1lvaySNKnEt6OW41Dwzcao3ZIFN7Y/h6+LYrZCp6++WlxzsffwoM75LHfxkdmJEdOHikb5nLGU2aB9YOSOwvKKfmEYQPl4nIJIA42qRRUgl3vqOW6JjJiNskmWXsw3X2cWHpI4P6L35EDE6svs1S6oLfDWEVaweH7XXxq4no3TMAgJyBOtVlJCV5iIxc141wy9pCZm03/5jOQzWmxQUm+QQ1LXd0AWxBQfVzAu9cc9XW9dOnkUHQH1hPTwMF0qyW0vyoo/Dk3lb7+RlV9eWnb2HaxMxhn2ZTBAYkFJFI/yWOfdi+R0KetW0oglgDX9bhyl8vFIgktPX6eViIiXS6fzfrzKLa1PCjN+WNqzJS4rIp4KckNPJfVkmKXg6Ko1JH2h8CphESqkNkHk+XTy3C0ogl3nwnhucG1SkFZfna9v910kQOAO3/6o8BNI1IhUAC5aNKt5PEke0bpyktseHhzHSvSwA3eUurxXN3WD+/gm2u1DyAmJRvb4DTpxkb2mjjDfMNnzVhc4J8uFg5rCFyL1JDWzix3Hw+mdYLsV2qMW2IuyEmcv+fYPW77+vsMxbP/S16luFm8V1T/ErKgOfX74VtfW/No8f/qHNAy/sIn+USV7wo3vQv1g+j/7S+b+Zji5dxlSb9EnCu+WyEjhj4IZBbhrSsn644cUb1UeXthX8vTM6eabwGVItVq38QwfiQ6nZMU4eUVH+6ejPbm8n0SWoHOX0PKvMuAnny+eRAts9yJ29giggiiveW8kbYEb3R2OJDuO/5qgQQHmroB3h1Dxbti1Awp90O3piYIg/XYGCqehNEwsrwH/LvvDnkgB4/94PeNLEVmfwK6y08G/iR5RORbMFBplOkryJyUozl4vDf+BqkzXxK941bj8Mj1xogdEvmXdn9Gbot422byk5/N/vcotzU8LMv7N7t9e/bruFmyvVO3FQuRRpq8smOUjSg8gqqoZo/pMlUNrnaWNoSmpIrCnKVVQucDopC00bYbK/yVnbCHO1PlBe4TV7bk6xUwIgUuvjtSxakfPwulKPVV/6rmFk+cPFE2wKWPJc8DH+Snq20vvWI6L9PFG+a1Jg7Po5fiqCVTLWcGpV/t+aazh+0Gsn870GUELJASvEUP9CDthMZHbx2lbfUyNDKiaVgphiPCzdI1swr9QFzgZpB6rKeqfYynerAslu5J9X8z+obiT/fEMlYLib7/N89D0dEULsi9cBONstRo59LjfwzahmiuMc/T3pDveXzMNWFk9BKq73+enIwCxtvfQoeWHfSlpIERYc5ulhQr/00sJrRg8T0Ww8ejw9aYlxsvGTvp8x9qzePZ2ADHGPcN493VA9AohgcN36FmxWB8KMPcHOui5IB7q6QtPeIDDwoWsHBuSo6adxW1p2YA4u2H8CVNNdV1dUJITiTEI9TUhxlDhLFUH6COOlBcHawC0SFW/vd9jtpYU1NXK4Q+JItHgLG3PXhP9E9C++u6OfS9cEdReBlvMP5cJzdLsiUlgAkNHLyP0eDryaSu0p709J92D17aWNVcARU47qHGhunKYt0gPDdzw5U0s5NXshlsE/d2dAPPflI/CAnSaSYQm3R0m4iEZrA5Cwg0UEFoI80xLPumOBxQFExwspKnln9YDocAsGzdhLNbMTZ2clS+wx/GQn9gEb/eA/bG/TjIiJ84Ao0dcIPHbiBye/dae3dw2x3sbYjxJLXXI8jOcT2oQPhUIJXN4b9yLqxzJ6/dididvXJn+wYXu7vodFdPGmtIkD1rO8soLRNwOMCCUBp8FA4+Fkpx9en4oYMaj9gPoth0EmoKQq9Vyh4cCChzMIIyN0rtbm/MHay4gx5XtFTkoA/lWcBRvg8HtvAxVvKGV4blFCaT9LJfXugkjrRUewWDo+VitcfIeGE0Gi/b3bBsNzjS2rf27rDM1sGdvbdkxqi3xJqUoUkW2LRiqGhx9DVw3TielbgDt3Fu2mB363RujE52USqXF7irqBnNjiFjDPxPyMfZcPPx0MZMQgwWS6l4YOuYMDN/zkIW70Ymr0EWpyCThyGLHyCTDWTxFmQyG1m8YEXZQmzFLpLh1iuSsMVmxPLEIIptzBNUU+C3ajjF0fBds4JsQ8T6n5dVPr6mcd7481LeZb7ZE1dTOJd/n0oeAwBq6VhrM95n2OMtlg61CKOtr/hp+RxtATlYHrJ2AthfVJr1PoDNlU9x1PpKyqv9sJ46BdF8vQDEErUSzIwXTKkNmF1ymzyB0eZaflr4xbQivYoI5t0pwlZZoRu5yui29cYcOeVXBONDe9SdsW11jecL1TABZ2bNtEpy6maMDOGxuEiUrK6oqd/kIXjmC4QYyWlIqxGPPOQUv9UvK59OENPVu4mPsgIjXR6oNyEE8h1svWdjlF9Dm2zje9SYVE3qmrGKgdUZqp62GKCPa0eojgrlPKrUVJ58hWWL5Io86zvAFSPNtPVVOTfilsAXxyQpK2jscG45F+JKlEFcGbsM4hYxXglVKy+LlFVqm4jAb/Xrkk9jeehSEuBhgfgOR9VUeXkI65cIoshfS1akS/FTp5KJIjplHfYAiEBk9euSd2N76FIS4GGs+AQsFlny0NrsFflrFZHQT1a/h5RP4TFvuqAga6Q4YqbgLqrlxMOaXYpajU+lrcSfSHWKeGJa8qkZdJv05Djz4+2kNHkPG8wuihP50daP8v9j89muqPJNPAJwF3vCTn1m7Gj/QXUbAODhjbXvAC+9t4d+t5qIXGlvPICCAQCCH6J2KosO48oBIHSYD3LTWfSwqX1a9g8lXvyzo3dc2W9ZO+holgD+Zf25uQ2Jh57lqiJRlTs8+cz6MLCsp9auJe1b2ob7ui7Spp7S/h+70no/jvDtaPXwqSt/4qQVtzfKRWwRfhedTi1LhKfTmNPt/kuhZlOcPVFYPl50UaYoZU+J1DNx7xJlpVvdicVER/wquYQke0JnbqxLlr2XVJGvgrJMe4eKvLuEmJvNQHUi+LMVDlTzGtP2ftQcLGASuzXJqQI2ZDvNq6yemsrS/kNtQIAPNMIUGlxjmtNjOu3CUaIA7Ghc6YNIXGNXzA3zIK61KwcJdWw6SEqz8iCFYeZMp5Zy1yLAriEOImjqNIjYnZRGSFLpBE3Val4tMEQuZcnca1gSIceLXCU1ldBRfbsKeJor5Otm7zXTCgea1M/nqISmarSzYn6cWImgfNdhB7rxhEhcGSZVfW3ZEC5U66csUryhq7Any2WwFhprqqkWTTvGLPCOfvXt9EIgWjrci2Q7G7tLsP2lXOFdVXKENYfYR4aCDrm7zSwapApLIi4nIO6iFUyIIVztOeKF9V08ReiMWjbpCXMiLFEubz2KXZ4kQtw5q+akC+tm0Txmc6LWf2rub/oHQIU4OLccDTWXq72uLl2584QmUv1Z5bGXhKqhG6aVG7mV27lJtx3X8/UGo8lssdrsDqcLghEUwwmSohmWQ6ijq6dvYAgggmI4QVK0kbGJqZm5hXMXLl25duPWnXsPHj159oJAotAYLA5PIJLIFCqNzmCy2Bx9Lo8vEIrEEqlMrlCq1BH21eq8evPuI196uUxdVu583rv/Lmcd7/Jq34/VN3f/T9etEYSwOKsdLD1eymH8vlmWOI/3PZtXzJltLVPdz+OSb+8JgwEFBAEFA4eAhIKGgYWDR7BizgIQBBQMHAISChoGFg4ewYo5B0AQUDBwCEgoaBhYOHgEK+Y8AEFAwcAhIKGgYWDh4BGsmAsABAEFA4eAhIKGgYWDR7BiLgIQDBwCEsrUVydgbPsxaFT+1x9jdZPf3eZxW3fFO4FEQUss/V31Ee85Err2blaKUr1fHUBXIUOrZ+RlEsIm/85OoL14QXsxUnvRT2S+BCgwlajK9n3/GE+OWF4+xa74+z4AAA==) + format("woff2"); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, + U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, + U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, + U+FFFD; +} + +/* cyrillic-ext */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 500; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 500; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 500; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAAAPwAA8AAAAAB6wAAAOWAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiYbgTYcOgZgP1NUQVRaAFQREAqBbIFwCxAAATYCJAMcBCAFhRYHSBuXBhAeibGthq3ESc/J4sXi4T89+O6bTPBpLoiK6kR2sKs9Wwn5ce339j/qyYa55zPdzRDPWgmVrBlPhEoDBNntJzniBYIHBWIJZ8SpnGzwaF5zoQCFLCKjc+jeVA11gZZwDepFQ8XppZIWzJd5plal33KmajsVEx1Tkfeerz29kAwGoKsQaIRQCaUkFAIdzbchcfXapS3A65D5gIcZHuBtqAzwUUQIWAK0rYSQnstDwfWkBFmsEAiXHytJpTukI8JWK5CVMoQs0olVu1SERsjg4HhFkMGslE8rXyxEaaUSaVNe8XHvYPrUVeWALQT7nC+LX5WUs/KbuAb9CaQz8xJQLAcBf2Jdr86iV4b95vz/7qL5P51L3Ah5ffmC4RKE0CjQWxyTxY4/UNgIcfzYAoE47BjFR8ViRiqSpJCkGAIQCEEMil8RSDGQDpAwhQLfmWA50FkjyehieG0p+6ptOb5itL+133/z1DfjIQAQCo2OuqjbVjeEGnRFxlJgH6A70BkApLaVenbr1PPki08++KDjOR999Fmc+fn3nc799tMTzvj0w1T/+PM4+7OzLR98cnI8frxLJ7iP8Yt31Iv/4/JB33TGdXc91vGMd9fsyX7v3qn3XS/9e8Td9/1/xJ0vb9/n7m8v3W7qMz99NenpK7ddN/b/yP7p57wbb379vH3SQ9fPp/9y05vnHADGvq8G7NZ1e9e1b/RZsNxacJdvexyTGp2o9y5c0zXlgen9v3tij36jjvmTx0mIvYBa9f/d7dUZov25HlW+2VGg8OI2+989tE+3od+aTg349I3n/oBvT/787mvbagZQSSD4iL76RuHk8QxIc6zOb3oWUm8oViudcCHfFrvOVxshZqmEW86SQHffCpE74L4Y3SEMj4ykq99RmO19Mk9QGugelaEu0dVsO+guz5TYQ4b5iZCYyQgdjcRAGfpODpoTg5mOxJBz2R4RUITmCQqfQ7OCYIL+srscOsG2y+Cw2IRlBp1DCqyKGELYpjrEyaOFyIBWrTgN44gIXAvO4WtBMJZWq2Ysab0y3bzY2mFgqhAx93dq0aYNJ1T5oFmrVs0Ooyc2FrNIPgXzmcbcOomIHGIxWliHNlgc22IzYO2hNQzhMmia1DgZNkTcclhduAivKYtD8pIqtzREMHJKEBafwUSEBK4VeSHCpVQvptFsrzPTyZpIUSjMjBxl/j/8WGxy+2R5tNO+tms1JqemAQAA) + format("woff2"); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 500; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* hebrew */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 500; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F; +} +/* math */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 500; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0330, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, + U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2034-2037, U+2057, U+20D0-20DC, U+20E1, + U+20E5-20EF, U+2102, U+210A-210E, U+2110-2112, U+2115, U+2119-211D, U+2124, U+2128, U+212C-212D, + U+212F-2131, U+2133-2138, U+213C-2140, U+2145-2149, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, + U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, + U+2336-237A, U+237C, U+2395, U+239B-23B6, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, + U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, + U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF; +} +/* symbols */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 500; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, + U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, + U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, + U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, + U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, + U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, + U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, + U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, + U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, + U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, + U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, + U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, + U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, + U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, + U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8B1, + U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, + U+1FA80-1FA88, U+1FA90-1FABD, U+1FABF-1FAC5, U+1FACE-1FADB, U+1FAE0-1FAE8, U+1FAF0-1FAF8, + U+1FB00-1FBFF; +} +/* vietnamese */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 500; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, + U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 500; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, + U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 500; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, + U+FFFD; +} + +/* cyrillic-ext */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} +/* cyrillic */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +/* greek-ext */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAAAPgAA8AAAAAB3gAAAOFAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiYbgTYcOgZgP1NUQVRaAFQREAqBcIFwCxAAATYCJAMcBCAFhGAHSBthBlFUTDYjfHFgG7MOeULKalqtE4ejm8yPYamkLBqeP4dyFA//7fev+8ydC0EEFdZA7ONYEjmQUeG2rn/5Ajli2bwjO5uNVKXPxKiSEnqpPnFu8UIY3ookxASCDj+gfmJdHcC+/WA+0k8HXQU1J8BR7P/UetT/cy5eC0z3ccMBjWwifVXWIZBiR3iUVMX3PIoqRQ6QDp52fhmYSL2ki5t0UZLMfxz/wQvJQEBnIdAIoRJKSSgE2ptrk+zK1ffvgPMvQsAVVgB4LF0AXk3FgDK0WhJCWheB+gt8HGrMIBCbHa1S6Sq0R1jnEbNcRcgiba1maYfQCBmNMFZvYaAWz7+XLy1IWUgl0oy89Y52gpgrrBPGEUTvnJ+nV0rKGbkNt2hK4E7Pm8BbCgL+xJp+OrMHDPnNlf9H8zFVEfchry1fYqgshEaBngpMlM7tVi4Lzq2rGHHgtBaygZLheqsUkhSDQCAEUaVXCKTobzdAwmQN/jJbCnTUSDI6GVoPjasWymOM1t1/P//9WAEAQqHRXid1iy4INeiMjMXYBnRFRwCQWqQ2hYilZOWfHNxgKu+OzsAF5/fYqtvT9PzDQ7qOj6PprFXn4CALPHUchgcNFxFl8OgomY6bI+6C8pv7p4H8TVNE4uVAgDa4AY+cmoLEkQP0wftqr2jh5Zm/UO3Tjz80OC+6UguLUcd+0fA0hFwr6k4VrggEmKdsGm+E6MDQHcYYd4x8iZoPqSgJV1u8WuE7h7lSTDvwXyxfHiPHG9Sqj3+qzaD1a/20bGsvUPjs27LeP24jLvyvwTbgi8+Xf+CHS+f1+1azhEoCwUdkBuGcXEXI2tW5zXtIPRGrtvrV6XwvutZWGyZZLA/NlwQ6e6EQuR2eRVVB6O5fSBq/QmGqV5AN9ABK/V1GKuIw09lMc1lBRxMhtDccKo1fob+OeiMDifbLHaQ9NiERTexK0YQ8hkdEiGhCXzwmxfXF4nG4FB5BSWxNMcUpBsGTUKSZUzyhmWMEx9x6J/eeJCyxN5qYNLUcCZk+WQTJQ2JohsmmmmrGgBMDhUbtay2wxpMnZzurzkoThXsk8U/czaPppsI71BYBsSpuW3wDgfgsBgUt801xEUFCY5+zy+Dfx1O833SX34VE0ryEOEIWG4kp0tyxDXbp40NJJtq6Y7qONZGiUJgeOcr8R3i71IpN+TNHvaV5Y6TncAQAAAA=) + format("woff2"); + unicode-range: U+1F00-1FFF; +} +/* greek */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF; +} +/* hebrew */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0590-05FF, U+200C-2010, U+20AA, U+25CC, U+FB1D-FB4F; +} +/* math */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0302-0303, U+0305, U+0307-0308, U+0330, U+0391-03A1, U+03A3-03A9, U+03B1-03C9, + U+03D1, U+03D5-03D6, U+03F0-03F1, U+03F4-03F5, U+2034-2037, U+2057, U+20D0-20DC, U+20E1, + U+20E5-20EF, U+2102, U+210A-210E, U+2110-2112, U+2115, U+2119-211D, U+2124, U+2128, U+212C-212D, + U+212F-2131, U+2133-2138, U+213C-2140, U+2145-2149, U+2190, U+2192, U+2194-21AE, U+21B0-21E5, + U+21F1-21F2, U+21F4-2211, U+2213-2214, U+2216-22FF, U+2308-230B, U+2310, U+2319, U+231C-2321, + U+2336-237A, U+237C, U+2395, U+239B-23B6, U+23D0, U+23DC-23E1, U+2474-2475, U+25AF, U+25B3, + U+25B7, U+25BD, U+25C1, U+25CA, U+25CC, U+25FB, U+266D-266F, U+27C0-27FF, U+2900-2AFF, + U+2B0E-2B11, U+2B30-2B4C, U+2BFE, U+FF5B, U+FF5D, U+1D400-1D7FF, U+1EE00-1EEFF; +} +/* symbols */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0001-000C, U+000E-001F, U+007F-009F, U+20DD-20E0, U+20E2-20E4, U+2150-218F, + U+2190, U+2192, U+2194-2199, U+21AF, U+21E6-21F0, U+21F3, U+2218-2219, U+2299, U+22C4-22C6, + U+2300-243F, U+2440-244A, U+2460-24FF, U+25A0-27BF, U+2800-28FF, U+2921-2922, U+2981, U+29BF, + U+29EB, U+2B00-2BFF, U+4DC0-4DFF, U+FFF9-FFFB, U+10140-1018E, U+10190-1019C, U+101A0, + U+101D0-101FD, U+102E0-102FB, U+10E60-10E7E, U+1D2C0-1D2D3, U+1D2E0-1D37F, U+1F000-1F0FF, + U+1F100-1F1AD, U+1F1E6-1F1FF, U+1F30D-1F30F, U+1F315, U+1F31C, U+1F31E, U+1F320-1F32C, U+1F336, + U+1F378, U+1F37D, U+1F382, U+1F393-1F39F, U+1F3A7-1F3A8, U+1F3AC-1F3AF, U+1F3C2, U+1F3C4-1F3C6, + U+1F3CA-1F3CE, U+1F3D4-1F3E0, U+1F3ED, U+1F3F1-1F3F3, U+1F3F5-1F3F7, U+1F408, U+1F415, U+1F41F, + U+1F426, U+1F43F, U+1F441-1F442, U+1F444, U+1F446-1F449, U+1F44C-1F44E, U+1F453, U+1F46A, + U+1F47D, U+1F4A3, U+1F4B0, U+1F4B3, U+1F4B9, U+1F4BB, U+1F4BF, U+1F4C8-1F4CB, U+1F4D6, U+1F4DA, + U+1F4DF, U+1F4E3-1F4E6, U+1F4EA-1F4ED, U+1F4F7, U+1F4F9-1F4FB, U+1F4FD-1F4FE, U+1F503, + U+1F507-1F50B, U+1F50D, U+1F512-1F513, U+1F53E-1F54A, U+1F54F-1F5FA, U+1F610, U+1F650-1F67F, + U+1F687, U+1F68D, U+1F691, U+1F694, U+1F698, U+1F6AD, U+1F6B2, U+1F6B9-1F6BA, U+1F6BC, + U+1F6C6-1F6CF, U+1F6D3-1F6D7, U+1F6E0-1F6EA, U+1F6F0-1F6F3, U+1F6F7-1F6FC, U+1F700-1F7FF, + U+1F800-1F80B, U+1F810-1F847, U+1F850-1F859, U+1F860-1F887, U+1F890-1F8AD, U+1F8B0-1F8B1, + U+1F900-1F90B, U+1F93B, U+1F946, U+1F984, U+1F996, U+1F9E9, U+1FA00-1FA6F, U+1FA70-1FA7C, + U+1FA80-1FA88, U+1FA90-1FABD, U+1FABF-1FAC5, U+1FACE-1FADB, U+1FAE0-1FAE8, U+1FAF0-1FAF8, + U+1FB00-1FBFF; +} +/* vietnamese */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, + U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} +/* latin-ext */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, + U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: "Open Sans"; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) + format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, + U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, + U+FFFD; +} diff --git a/apps/browser/src/autofill/spec/autofill-mocks.ts b/apps/browser/src/autofill/spec/autofill-mocks.ts index 021b7719b2b..d6cca2c04f6 100644 --- a/apps/browser/src/autofill/spec/autofill-mocks.ts +++ b/apps/browser/src/autofill/spec/autofill-mocks.ts @@ -7,16 +7,19 @@ 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 { OverlayCipherData } from "../background/abstractions/overlay.background"; +import { + FocusedFieldData, + InlineMenuCipherData, +} from "../background/abstractions/overlay.background"; import AutofillField from "../models/autofill-field"; import AutofillForm from "../models/autofill-form"; import AutofillPageDetails from "../models/autofill-page-details"; import AutofillScript, { FillScript } from "../models/autofill-script"; -import { InitAutofillOverlayButtonMessage } from "../overlay/abstractions/autofill-overlay-button"; -import { InitAutofillOverlayListMessage } from "../overlay/abstractions/autofill-overlay-list"; +import { InitAutofillInlineMenuButtonMessage } from "../overlay/inline-menu/abstractions/autofill-inline-menu-button"; +import { InitAutofillInlineMenuListMessage } from "../overlay/inline-menu/abstractions/autofill-inline-menu-list"; import { GenerateFillScriptOptions, PageDetail } from "../services/abstractions/autofill.service"; -function createAutofillFormMock(customFields = {}): AutofillForm { +export function createAutofillFormMock(customFields = {}): AutofillForm { return { opid: "default-form-opid", htmlID: "default-htmlID", @@ -27,7 +30,7 @@ function createAutofillFormMock(customFields = {}): AutofillForm { }; } -function createAutofillFieldMock(customFields = {}): AutofillField { +export function createAutofillFieldMock(customFields = {}): AutofillField { return { opid: "default-input-field-opid", elementNumber: 0, @@ -57,7 +60,7 @@ function createAutofillFieldMock(customFields = {}): AutofillField { }; } -function createPageDetailMock(customFields = {}): PageDetail { +export function createPageDetailMock(customFields = {}): PageDetail { return { frameId: 0, tab: createChromeTabMock(), @@ -66,7 +69,7 @@ function createPageDetailMock(customFields = {}): PageDetail { }; } -function createAutofillPageDetailsMock(customFields = {}): AutofillPageDetails { +export function createAutofillPageDetailsMock(customFields = {}): AutofillPageDetails { return { title: "title", url: "url", @@ -86,7 +89,7 @@ function createAutofillPageDetailsMock(customFields = {}): AutofillPageDetails { }; } -function createChromeTabMock(customFields = {}): chrome.tabs.Tab { +export function createChromeTabMock(customFields = {}): chrome.tabs.Tab { return { id: 1, index: 1, @@ -104,13 +107,14 @@ function createChromeTabMock(customFields = {}): chrome.tabs.Tab { }; } -function createGenerateFillScriptOptionsMock(customFields = {}): GenerateFillScriptOptions { +export function createGenerateFillScriptOptionsMock(customFields = {}): GenerateFillScriptOptions { return { skipUsernameOnlyFill: false, onlyEmptyFields: false, onlyVisibleFields: false, fillNewPassword: false, allowTotpAutofill: false, + autoSubmitLogin: false, cipher: mock(), tabUrl: "https://jest-testing-website.com", defaultUriMatch: UriMatchStrategy.Domain, @@ -118,7 +122,7 @@ function createGenerateFillScriptOptionsMock(customFields = {}): GenerateFillScr }; } -function createAutofillScriptMock( +export function createAutofillScriptMock( customFields = {}, scriptTypes?: Record, ): AutofillScript { @@ -159,28 +163,35 @@ const overlayPagesTranslations = { unlockYourAccount: "unlockYourAccount", unlockAccount: "unlockAccount", fillCredentialsFor: "fillCredentialsFor", - partialUsername: "partialUsername", + username: "username", view: "view", noItemsToShow: "noItemsToShow", newItem: "newItem", addNewVaultItem: "addNewVaultItem", }; -function createInitAutofillOverlayButtonMessageMock( +export function createInitAutofillInlineMenuButtonMessageMock( customFields = {}, -): InitAutofillOverlayButtonMessage { +): InitAutofillInlineMenuButtonMessage { return { - command: "initAutofillOverlayButton", + command: "initAutofillInlineMenuButton", translations: overlayPagesTranslations, styleSheetUrl: "https://jest-testing-website.com", authStatus: AuthenticationStatus.Unlocked, + portKey: "portKey", ...customFields, }; } -function createAutofillOverlayCipherDataMock(index: number, customFields = {}): OverlayCipherData { +export function createAutofillOverlayCipherDataMock( + index: number, + customFields = {}, +): InlineMenuCipherData { 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, @@ -194,15 +205,17 @@ function createAutofillOverlayCipherDataMock(index: number, customFields = {}): }; } -function createInitAutofillOverlayListMessageMock( +export function createInitAutofillInlineMenuListMessageMock( customFields = {}, -): InitAutofillOverlayListMessage { +): InitAutofillInlineMenuListMessage { return { - command: "initAutofillOverlayList", + command: "initAutofillInlineMenuList", translations: overlayPagesTranslations, styleSheetUrl: "https://jest-testing-website.com", theme: ThemeType.Light, authStatus: AuthenticationStatus.Unlocked, + portKey: "portKey", + filledByCipherType: CipherType.Login, ciphers: [ createAutofillOverlayCipherDataMock(1, { icon: { @@ -237,7 +250,9 @@ function createInitAutofillOverlayListMessageMock( }; } -function createFocusedFieldDataMock(customFields = {}) { +export function createFocusedFieldDataMock( + customFields: Partial = {}, +): FocusedFieldData { return { focusedFieldRects: { top: 1, @@ -249,12 +264,14 @@ function createFocusedFieldDataMock(customFields = {}) { paddingRight: "6px", paddingLeft: "6px", }, + filledByCipherType: CipherType.Login, tabId: 1, + frameId: 2, ...customFields, }; } -function createPortSpyMock(name: string) { +export function createPortSpyMock(name: string) { return mock({ name, onMessage: { @@ -273,16 +290,17 @@ function createPortSpyMock(name: string) { }); } -export { - createAutofillFormMock, - createAutofillFieldMock, - createPageDetailMock, - createAutofillPageDetailsMock, - createChromeTabMock, - createGenerateFillScriptOptionsMock, - createAutofillScriptMock, - createInitAutofillOverlayButtonMessageMock, - createInitAutofillOverlayListMessageMock, - createFocusedFieldDataMock, - createPortSpyMock, -}; +export function createMutationRecordMock(customFields = {}): MutationRecord { + return { + addedNodes: mock(), + attributeName: "default-attributeName", + attributeNamespace: "default-attributeNamespace", + nextSibling: null, + oldValue: "default-oldValue", + previousSibling: null, + removedNodes: mock(), + target: null, + type: "attributes", + ...customFields, + }; +} diff --git a/apps/browser/src/autofill/spec/testing-utils.ts b/apps/browser/src/autofill/spec/testing-utils.ts index 5b0db5ebd6f..4bbcd72dda2 100644 --- a/apps/browser/src/autofill/spec/testing-utils.ts +++ b/apps/browser/src/autofill/spec/testing-utils.ts @@ -1,21 +1,21 @@ import { mock } from "jest-mock-extended"; -function triggerTestFailure() { +export function triggerTestFailure() { expect(true).toBe("Test has failed."); } const scheduler = typeof setImmediate === "function" ? setImmediate : setTimeout; -function flushPromises() { +export function flushPromises() { return new Promise(function (resolve) { scheduler(resolve); }); } -function postWindowMessage(data: any, origin = "https://localhost/", source = window) { +export function postWindowMessage(data: any, origin = "https://localhost/", source = window) { globalThis.dispatchEvent(new MessageEvent("message", { data, origin, source })); } -function sendMockExtensionMessage( +export function sendMockExtensionMessage( message: any, sender?: chrome.runtime.MessageSender, sendResponse?: CallableFunction, @@ -32,7 +32,7 @@ function sendMockExtensionMessage( ); } -function triggerRuntimeOnConnectEvent(port: chrome.runtime.Port) { +export function triggerRuntimeOnConnectEvent(port: chrome.runtime.Port) { (chrome.runtime.onConnect.addListener as unknown as jest.SpyInstance).mock.calls.forEach( (call) => { const callback = call[0]; @@ -41,21 +41,37 @@ function triggerRuntimeOnConnectEvent(port: chrome.runtime.Port) { ); } -function sendPortMessage(port: chrome.runtime.Port, message: any) { +export function sendPortMessage(port: chrome.runtime.Port, message: any) { (port.onMessage.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => { const callback = call[0]; callback(message || {}, port); }); } -function triggerPortOnDisconnectEvent(port: chrome.runtime.Port) { +export function triggerPortOnConnectEvent(port: chrome.runtime.Port) { + (chrome.runtime.onConnect.addListener as unknown as jest.SpyInstance).mock.calls.forEach( + (call) => { + const callback = call[0]; + callback(port); + }, + ); +} + +export function triggerPortOnMessageEvent(port: chrome.runtime.Port, message: any) { + (port.onMessage.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => { + const callback = call[0]; + callback(message, port); + }); +} + +export function triggerPortOnDisconnectEvent(port: chrome.runtime.Port) { (port.onDisconnect.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => { const callback = call[0]; callback(port); }); } -function triggerWindowOnFocusedChangedEvent(windowId: number) { +export function triggerWindowOnFocusedChangedEvent(windowId: number) { (chrome.windows.onFocusChanged.addListener as unknown as jest.SpyInstance).mock.calls.forEach( (call) => { const callback = call[0]; @@ -64,7 +80,7 @@ function triggerWindowOnFocusedChangedEvent(windowId: number) { ); } -function triggerTabOnActivatedEvent(activeInfo: chrome.tabs.TabActiveInfo) { +export function triggerTabOnActivatedEvent(activeInfo: chrome.tabs.TabActiveInfo) { (chrome.tabs.onActivated.addListener as unknown as jest.SpyInstance).mock.calls.forEach( (call) => { const callback = call[0]; @@ -73,14 +89,14 @@ function triggerTabOnActivatedEvent(activeInfo: chrome.tabs.TabActiveInfo) { ); } -function triggerTabOnReplacedEvent(addedTabId: number, removedTabId: number) { +export function triggerTabOnReplacedEvent(addedTabId: number, removedTabId: number) { (chrome.tabs.onReplaced.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => { const callback = call[0]; callback(addedTabId, removedTabId); }); } -function triggerTabOnUpdatedEvent( +export function triggerTabOnUpdatedEvent( tabId: number, changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab, @@ -91,14 +107,74 @@ function triggerTabOnUpdatedEvent( }); } -function triggerTabOnRemovedEvent(tabId: number, removeInfo: chrome.tabs.TabRemoveInfo) { +export function triggerTabOnRemovedEvent(tabId: number, removeInfo: chrome.tabs.TabRemoveInfo) { (chrome.tabs.onRemoved.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => { const callback = call[0]; callback(tabId, removeInfo); }); } -function mockQuerySelectorAllDefinedCall() { +export function triggerOnAlarmEvent(alarm: chrome.alarms.Alarm) { + (chrome.alarms.onAlarm.addListener as unknown as jest.SpyInstance).mock.calls.forEach((call) => { + const callback = call[0]; + callback(alarm); + }); +} + +export function triggerWebNavigationOnCommittedEvent( + details: chrome.webNavigation.WebNavigationFramedCallbackDetails, +) { + (chrome.webNavigation.onCommitted.addListener as unknown as jest.SpyInstance).mock.calls.forEach( + (call) => { + const callback = call[0]; + callback(details); + }, + ); +} + +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 triggerWebRequestOnCompletedEvent(details: chrome.webRequest.WebRequestDetails) { + (chrome.webRequest.onCompleted.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) { return originalDocumentQuerySelectorAll.call( @@ -125,19 +201,3 @@ function mockQuerySelectorAllDefinedCall() { }, }; } - -export { - triggerTestFailure, - flushPromises, - postWindowMessage, - sendMockExtensionMessage, - triggerRuntimeOnConnectEvent, - sendPortMessage, - triggerPortOnDisconnectEvent, - triggerWindowOnFocusedChangedEvent, - triggerTabOnActivatedEvent, - triggerTabOnReplacedEvent, - triggerTabOnUpdatedEvent, - triggerTabOnRemovedEvent, - mockQuerySelectorAllDefinedCall, -}; diff --git a/apps/browser/src/autofill/utils/autofill-overlay.enum.ts b/apps/browser/src/autofill/utils/autofill-overlay.enum.ts deleted file mode 100644 index 486d68f7540..00000000000 --- a/apps/browser/src/autofill/utils/autofill-overlay.enum.ts +++ /dev/null @@ -1,17 +0,0 @@ -const AutofillOverlayElement = { - Button: "autofill-overlay-button", - List: "autofill-overlay-list", -} as const; - -const AutofillOverlayPort = { - Button: "autofill-overlay-button-port", - List: "autofill-overlay-list-port", -} as const; - -const RedirectFocusDirection = { - Current: "current", - Previous: "previous", - Next: "next", -} as const; - -export { AutofillOverlayElement, AutofillOverlayPort, RedirectFocusDirection }; diff --git a/apps/browser/src/autofill/utils/index.spec.ts b/apps/browser/src/autofill/utils/index.spec.ts index dcb5aa64696..36d22ed0cd3 100644 --- a/apps/browser/src/autofill/utils/index.spec.ts +++ b/apps/browser/src/autofill/utils/index.spec.ts @@ -1,4 +1,4 @@ -import { AutofillPort } from "../enums/autofill-port.enums"; +import { AutofillPort } from "../enums/autofill-port.enum"; import { triggerPortOnDisconnectEvent } from "../spec/testing-utils"; import { logoIcon, logoLockedIcon } from "./svg-icons"; @@ -38,9 +38,7 @@ describe("generateRandomCustomElementName", () => { describe("sendExtensionMessage", () => { it("sends a message to the extension", async () => { - const extensionMessagePromise = sendExtensionMessage("updateAutofillOverlayHidden", { - display: "none", - }); + const extensionMessagePromise = sendExtensionMessage("some-extension-message"); // Jest doesn't give anyway to select the typed overload of "sendMessage", // a cast is needed to get the correct spy type. @@ -58,7 +56,6 @@ describe("sendExtensionMessage", () => { responseCallback("sendMessageResponse"); const response = await extensionMessagePromise; - expect(response).toEqual("sendMessageResponse"); }); }); diff --git a/apps/browser/src/autofill/utils/index.ts b/apps/browser/src/autofill/utils/index.ts index 873012d1dbb..7c18e7fd127 100644 --- a/apps/browser/src/autofill/utils/index.ts +++ b/apps/browser/src/autofill/utils/index.ts @@ -1,5 +1,24 @@ -import { AutofillPort } from "../enums/autofill-port.enums"; -import { FillableFormFieldElement, FormFieldElement } from "../types"; +import { AutofillPort } from "../enums/autofill-port.enum"; +import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types"; + +/** + * Generates a random string of characters. + * + * @param length - The length of the random string to generate. + */ +export function generateRandomChars(length: number): string { + const chars = "abcdefghijklmnopqrstuvwxyz"; + const randomChars = []; + const randomBytes = new Uint8Array(length); + globalThis.crypto.getRandomValues(randomBytes); + + for (let byteIndex = 0; byteIndex < randomBytes.length; byteIndex++) { + const byte = randomBytes[byteIndex]; + randomChars.push(chars[byte % chars.length]); + } + + return randomChars.join(""); +} /** * Polyfills the requestIdleCallback API with a setTimeout fallback. @@ -34,21 +53,7 @@ export function cancelIdleCallbackPolyfill(id: NodeJS.Timeout | number) { /** * Generates a random string of characters that formatted as a custom element name. */ -function generateRandomCustomElementName(): string { - const generateRandomChars = (length: number): string => { - const chars = "abcdefghijklmnopqrstuvwxyz"; - const randomChars = []; - const randomBytes = new Uint8Array(length); - globalThis.crypto.getRandomValues(randomBytes); - - for (let byteIndex = 0; byteIndex < randomBytes.length; byteIndex++) { - const byte = randomBytes[byteIndex]; - randomChars.push(chars[byte % chars.length]); - } - - return randomChars.join(""); - }; - +export function generateRandomCustomElementName(): string { const length = Math.floor(Math.random() * 5) + 8; // Between 8 and 12 characters const numHyphens = Math.min(Math.max(Math.floor(Math.random() * 4), 1), length - 1); // At least 1, maximum of 3 hyphens @@ -81,7 +86,7 @@ function generateRandomCustomElementName(): string { * @param svgString - The SVG string to build the DOM element from. * @param ariaHidden - Determines whether the SVG should be hidden from screen readers. */ -function buildSvgDomElement(svgString: string, ariaHidden = true): HTMLElement { +export function buildSvgDomElement(svgString: string, ariaHidden = true): HTMLElement { const domParser = new DOMParser(); const svgDom = domParser.parseFromString(svgString, "image/svg+xml"); const domElement = svgDom.documentElement; @@ -96,19 +101,27 @@ function buildSvgDomElement(svgString: string, ariaHidden = true): HTMLElement { * @param command - The command to send. * @param options - The options to send with the command. */ -async function sendExtensionMessage( +export async function sendExtensionMessage( command: string, options: Record = {}, -): Promise { - return new Promise((resolve) => { +): Promise { + if ( + typeof browser !== "undefined" && + typeof browser.runtime !== "undefined" && + typeof browser.runtime.sendMessage !== "undefined" + ) { + return browser.runtime.sendMessage({ command, ...options }); + } + + return new Promise((resolve) => chrome.runtime.sendMessage(Object.assign({ command }, options), (response) => { if (chrome.runtime.lastError) { - return; + resolve(null); } resolve(response); - }); - }); + }), + ); } /** @@ -118,7 +131,7 @@ async function sendExtensionMessage( * @param styles - The styles to set on the element. * @param priority - Determines whether the styles should be set as important. */ -function setElementStyles( +export function setElementStyles( element: HTMLElement, styles: Partial, priority?: boolean, @@ -141,9 +154,9 @@ function setElementStyles( * and triggers an onDisconnect event if the extension context * is invalidated. * - * @param callback - Callback function to run when the extension disconnects + * @param callback - Callback export function to run when the extension disconnects */ -function setupExtensionDisconnectAction(callback: (port: chrome.runtime.Port) => void) { +export function setupExtensionDisconnectAction(callback: (port: chrome.runtime.Port) => void) { const port = chrome.runtime.connect({ name: AutofillPort.InjectedScript }); const onDisconnectCallback = (disconnectedPort: chrome.runtime.Port) => { callback(disconnectedPort); @@ -158,7 +171,7 @@ function setupExtensionDisconnectAction(callback: (port: chrome.runtime.Port) => * * @param windowContext - The global window context */ -function setupAutofillInitDisconnectAction(windowContext: Window) { +export function setupAutofillInitDisconnectAction(windowContext: Window) { if (!windowContext.bitwardenAutofillInit) { return; } @@ -176,10 +189,10 @@ function setupAutofillInitDisconnectAction(windowContext: Window) { * * @param formFieldElement - The form field element to check. */ -function elementIsFillableFormField( +export function elementIsFillableFormField( formFieldElement: FormFieldElement, ): formFieldElement is FillableFormFieldElement { - return formFieldElement?.tagName.toLowerCase() !== "span"; + return !elementIsSpanElement(formFieldElement); } /** @@ -188,8 +201,11 @@ function elementIsFillableFormField( * @param element - The element to check. * @param tagName - The tag name to check against. */ -function elementIsInstanceOf(element: Element, tagName: string): element is T { - return element?.tagName.toLowerCase() === tagName; +export function elementIsInstanceOf( + element: Element, + tagName: string, +): element is T { + return nodeIsElement(element) && element.tagName.toLowerCase() === tagName; } /** @@ -197,7 +213,7 @@ function elementIsInstanceOf(element: Element, tagName: strin * * @param element - The element to check. */ -function elementIsSpanElement(element: Element): element is HTMLSpanElement { +export function elementIsSpanElement(element: Element): element is HTMLSpanElement { return elementIsInstanceOf(element, "span"); } @@ -206,7 +222,7 @@ function elementIsSpanElement(element: Element): element is HTMLSpanElement { * * @param element - The element to check. */ -function elementIsInputElement(element: Element): element is HTMLInputElement { +export function elementIsInputElement(element: Element): element is HTMLInputElement { return elementIsInstanceOf(element, "input"); } @@ -215,7 +231,7 @@ function elementIsInputElement(element: Element): element is HTMLInputElement { * * @param element - The element to check. */ -function elementIsSelectElement(element: Element): element is HTMLSelectElement { +export function elementIsSelectElement(element: Element): element is HTMLSelectElement { return elementIsInstanceOf(element, "select"); } @@ -224,7 +240,7 @@ function elementIsSelectElement(element: Element): element is HTMLSelectElement * * @param element - The element to check. */ -function elementIsTextAreaElement(element: Element): element is HTMLTextAreaElement { +export function elementIsTextAreaElement(element: Element): element is HTMLTextAreaElement { return elementIsInstanceOf(element, "textarea"); } @@ -233,7 +249,7 @@ function elementIsTextAreaElement(element: Element): element is HTMLTextAreaElem * * @param element - The element to check. */ -function elementIsFormElement(element: Element): element is HTMLFormElement { +export function elementIsFormElement(element: Element): element is HTMLFormElement { return elementIsInstanceOf(element, "form"); } @@ -242,7 +258,7 @@ function elementIsFormElement(element: Element): element is HTMLFormElement { * * @param element - The element to check. */ -function elementIsLabelElement(element: Element): element is HTMLLabelElement { +export function elementIsLabelElement(element: Element): element is HTMLLabelElement { return elementIsInstanceOf(element, "label"); } @@ -251,7 +267,7 @@ function elementIsLabelElement(element: Element): element is HTMLLabelElement { * * @param element - The element to check. */ -function elementIsDescriptionDetailsElement(element: Element): element is HTMLElement { +export function elementIsDescriptionDetailsElement(element: Element): element is HTMLElement { return elementIsInstanceOf(element, "dd"); } @@ -260,7 +276,7 @@ function elementIsDescriptionDetailsElement(element: Element): element is HTMLEl * * @param element - The element to check. */ -function elementIsDescriptionTermElement(element: Element): element is HTMLElement { +export function elementIsDescriptionTermElement(element: Element): element is HTMLElement { return elementIsInstanceOf(element, "dt"); } @@ -269,12 +285,12 @@ function elementIsDescriptionTermElement(element: Element): element is HTMLEleme * * @param node - The node to check. */ -function nodeIsElement(node: Node): node is Element { +export function nodeIsElement(node: Node): node is Element { if (!node) { return false; } - return node.nodeType === Node.ELEMENT_NODE; + return node?.nodeType === Node.ELEMENT_NODE; } /** @@ -282,7 +298,7 @@ function nodeIsElement(node: Node): node is Element { * * @param node - The node to check. */ -function nodeIsInputElement(node: Node): node is HTMLInputElement { +export function nodeIsInputElement(node: Node): node is HTMLInputElement { return nodeIsElement(node) && elementIsInputElement(node); } @@ -291,28 +307,96 @@ function nodeIsInputElement(node: Node): node is HTMLInputElement { * * @param node - The node to check. */ -function nodeIsFormElement(node: Node): node is HTMLFormElement { +export function nodeIsFormElement(node: Node): node is HTMLFormElement { return nodeIsElement(node) && elementIsFormElement(node); } -export { - generateRandomCustomElementName, - buildSvgDomElement, - sendExtensionMessage, - setElementStyles, - setupExtensionDisconnectAction, - setupAutofillInitDisconnectAction, - elementIsFillableFormField, - elementIsInstanceOf, - elementIsSpanElement, - elementIsInputElement, - elementIsSelectElement, - elementIsTextAreaElement, - elementIsFormElement, - elementIsLabelElement, - elementIsDescriptionDetailsElement, - elementIsDescriptionTermElement, - nodeIsElement, - nodeIsInputElement, - nodeIsFormElement, -}; +/** + * Returns a boolean representing the attribute value of an element. + * + * @param element + * @param attributeName + * @param checkString + */ +export function getAttributeBoolean( + element: HTMLElement, + attributeName: string, + checkString = false, +): boolean { + if (checkString) { + return getPropertyOrAttribute(element, attributeName) === "true"; + } + + return Boolean(getPropertyOrAttribute(element, attributeName)); +} + +/** + * Get the value of a property or attribute from a FormFieldElement. + * + * @param element + * @param attributeName + */ +export function getPropertyOrAttribute(element: HTMLElement, attributeName: string): string | null { + if (attributeName in element) { + return (element as FormElementWithAttribute)[attributeName]; + } + + return element.getAttribute(attributeName); +} + +/** + * Throttles a callback function to run at most once every `limit` milliseconds. + * + * @param callback - The callback function to throttle. + * @param limit - The time in milliseconds to throttle the callback. + */ +export function throttle(callback: (_args: any) => any, limit: number) { + let waitingDelay = false; + return function (...args: unknown[]) { + if (!waitingDelay) { + callback.apply(this, args); + waitingDelay = true; + globalThis.setTimeout(() => (waitingDelay = false), limit); + } + }; +} + +/** + * Gathers and normalizes keywords from a potential submit button element. Used + * to verify if the element submits a login or change password form. + * + * @param element - The element to gather keywords from. + */ +export function getSubmitButtonKeywordsSet(element: HTMLElement): Set { + const keywords = [ + element.textContent, + element.getAttribute("type"), + 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"), + ]; + + const keywordsSet = new Set(); + for (let i = 0; i < keywords.length; i++) { + if (typeof keywords[i] === "string") { + // Iterate over all keywords metadata and split them by non-letter characters. + // This ensures we check against individual words and not the entire string. + keywords[i] + .toLowerCase() + .replace(/[-\s]/g, "") + .split(/[^\p{L}]+/gu) + .forEach((keyword) => { + if (keyword) { + keywordsSet.add(keyword); + } + }); + } + } + + return keywordsSet; +} diff --git a/apps/browser/src/autofill/utils/svg-icons.ts b/apps/browser/src/autofill/utils/svg-icons.ts index a3a8de4d4f0..df2cfa189f9 100644 --- a/apps/browser/src/autofill/utils/svg-icons.ts +++ b/apps/browser/src/autofill/utils/svg-icons.ts @@ -1,19 +1,29 @@ -const logoIcon = +export const logoIcon = ''; -const logoLockedIcon = +export const logoLockedIcon = ''; -const globeIcon = +export const globeIcon = ''; -const lockIcon = +export const creditCardIcon = + ''; + +export const idCardIcon = + ''; + +export const lockIcon = ''; -const plusIcon = - ''; +export const plusIcon = + ''; -const viewCipherIcon = +export const viewCipherIcon = ''; -export { logoIcon, logoLockedIcon, globeIcon, lockIcon, plusIcon, viewCipherIcon }; +export const passkeyIcon = + ''; + +export const circleCheckIcon = + ''; diff --git a/apps/browser/src/background/commands.background.ts b/apps/browser/src/background/commands.background.ts index f2152c7f9f4..b87f5d0fcfb 100644 --- a/apps/browser/src/background/commands.background.ts +++ b/apps/browser/src/background/commands.background.ts @@ -1,6 +1,7 @@ import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { ExtensionCommand, ExtensionCommandType } from "@bitwarden/common/autofill/constants"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @@ -25,7 +26,7 @@ export default class CommandsBackground { this.isVivaldi = this.platformUtilsService.isVivaldi(); } - async init() { + init() { BrowserApi.messageListener("commands.background", (msg: any) => { if (msg.command === "unlockCompleted" && msg.data.target === "commands.background") { this.processCommand( @@ -47,8 +48,23 @@ export default class CommandsBackground { case "generate_password": await this.generatePasswordToClipboard(); break; - case "autofill_login": - await this.autoFillLogin(sender ? sender.tab : null); + case ExtensionCommand.AutofillLogin: + await this.triggerAutofillCommand( + sender ? sender.tab : null, + ExtensionCommand.AutofillCommand, + ); + break; + case ExtensionCommand.AutofillCard: + await this.triggerAutofillCommand( + sender ? sender.tab : null, + ExtensionCommand.AutofillCard, + ); + break; + case ExtensionCommand.AutofillIdentity: + await this.triggerAutofillCommand( + sender ? sender.tab : null, + ExtensionCommand.AutofillIdentity, + ); break; case "open_popup": await this.openPopup(); @@ -68,19 +84,27 @@ export default class CommandsBackground { await this.passwordGenerationService.addHistory(password); } - private async autoFillLogin(tab?: chrome.tabs.Tab) { + private async triggerAutofillCommand( + tab?: chrome.tabs.Tab, + commandSender?: ExtensionCommandType, + ) { if (!tab) { tab = await BrowserApi.getTabFromCurrentWindowId(); } - if (tab == null) { + if (tab == null || !commandSender) { return; } if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) { const retryMessage: LockedVaultPendingNotificationsData = { commandToRetry: { - message: { command: "autofill_login" }, + message: { + command: + commandSender === ExtensionCommand.AutofillCommand + ? ExtensionCommand.AutofillLogin + : commandSender, + }, sender: { tab: tab }, }, target: "commands.background", @@ -95,7 +119,7 @@ export default class CommandsBackground { return; } - await this.main.collectPageDetailsForContentScript(tab, "autofill_cmd"); + await this.main.collectPageDetailsForContentScript(tab, commandSender); } private async openPopup() { diff --git a/apps/browser/src/background/idle.background.ts b/apps/browser/src/background/idle.background.ts index eef033b364b..a5d50e8508f 100644 --- a/apps/browser/src/background/idle.background.ts +++ b/apps/browser/src/background/idle.background.ts @@ -7,8 +7,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; -import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; - const IdleInterval = 60 * 5; // 5 minutes export default class IdleBackground { @@ -18,7 +16,6 @@ export default class IdleBackground { constructor( private vaultTimeoutService: VaultTimeoutService, - private stateService: BrowserStateService, private notificationsService: NotificationsService, private accountService: AccountService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, @@ -26,7 +23,7 @@ export default class IdleBackground { this.idle = chrome.idle || (browser != null ? browser.idle : null); } - async init() { + init() { if (!this.idle) { return; } diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 8de3014fb2c..1cb615fe067 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -72,12 +72,14 @@ import { import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; import { ClientType } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { Fido2ActiveRequestManager as Fido2ActiveRequestManagerAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-active-request-manager.abstraction"; import { Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-authenticator.service.abstraction"; import { Fido2ClientService as Fido2ClientServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-client.service.abstraction"; import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction"; @@ -86,6 +88,7 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService, ObservableStorageService, @@ -95,27 +98,34 @@ import { BiometricStateService, DefaultBiometricStateService, } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency creation import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { Lazy } from "@bitwarden/common/platform/misc/lazy"; import { clearCaches } from "@bitwarden/common/platform/misc/sequentialize"; +import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { BulkEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/bulk-encrypt.service.implementation"; 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"; import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; +import { StateService } from "@bitwarden/common/platform/services/state.service"; import { SystemService } from "@bitwarden/common/platform/services/system.service"; import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; @@ -135,6 +145,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"; @@ -193,55 +205,61 @@ import { VaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; +import { OverlayNotificationsBackground as OverlayNotificationsBackgroundInterface } from "../autofill/background/abstractions/overlay-notifications.background"; +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"; +import { OverlayNotificationsBackground } from "../autofill/background/overlay-notifications.background"; +import { OverlayBackground } from "../autofill/background/overlay.background"; import TabsBackground from "../autofill/background/tabs.background"; import WebRequestBackground from "../autofill/background/web-request.background"; import { CipherContextMenuHandler } from "../autofill/browser/cipher-context-menu-handler"; import { ContextMenuClickedHandler } from "../autofill/browser/context-menu-clicked-handler"; import { MainContextMenuHandler } from "../autofill/browser/main-context-menu-handler"; +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"; -import { Account } from "../models/account"; import { BrowserApi } from "../platform/browser/browser-api"; -import { flagEnabled } from "../platform/flags"; import { UpdateBadge } from "../platform/listeners/update-badge"; /* eslint-disable no-restricted-imports */ import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender"; /* eslint-enable no-restricted-imports */ import { OffscreenDocumentService } from "../platform/offscreen-document/abstractions/offscreen-document"; import { DefaultOffscreenDocumentService } from "../platform/offscreen-document/offscreen-document.service"; -import { BrowserStateService as StateServiceAbstraction } from "../platform/services/abstractions/browser-state.service"; +import { BrowserTaskSchedulerService } from "../platform/services/abstractions/browser-task-scheduler.service"; +import { BackgroundBrowserBiometricsService } from "../platform/services/background-browser-biometrics.service"; import { BrowserCryptoService } from "../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../platform/services/browser-local-storage.service"; import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service"; import { BrowserScriptInjectorService } from "../platform/services/browser-script-injector.service"; -import { DefaultBrowserStateService } from "../platform/services/default-browser-state.service"; import I18nService from "../platform/services/i18n.service"; import { 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"; import IdleBackground from "./idle.background"; import { NativeMessagingBackground } from "./nativeMessaging.background"; import RuntimeBackground from "./runtime.background"; - export default class MainBackground { messagingService: MessageSender; storageService: BrowserLocalStorageService; @@ -300,12 +318,14 @@ export default class MainBackground { vaultFilterService: VaultFilterService; usernameGenerationService: UsernameGenerationServiceAbstraction; encryptService: EncryptService; + bulkEncryptService: FallbackBulkEncryptService; folderApiService: FolderApiServiceAbstraction; policyApiService: PolicyApiServiceAbstraction; sendApiService: SendApiServiceAbstraction; userVerificationApiService: UserVerificationApiServiceAbstraction; fido2UserInterfaceService: Fido2UserInterfaceServiceAbstraction; fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction; + fido2ActiveRequestManager: Fido2ActiveRequestManagerAbstraction; fido2ClientService: Fido2ClientServiceAbstraction; avatarService: AvatarServiceAbstraction; mainContextMenuHandler: MainContextMenuHandler; @@ -323,11 +343,13 @@ export default class MainBackground { activeUserStateProvider: ActiveUserStateProvider; derivedStateProvider: DerivedStateProvider; stateProvider: StateProvider; + taskSchedulerService: BrowserTaskSchedulerService; fido2Background: Fido2BackgroundAbstraction; individualVaultExportService: IndividualVaultExportServiceAbstraction; organizationVaultExportService: OrganizationVaultExportServiceAbstraction; vaultSettingsService: VaultSettingsServiceAbstraction; biometricStateService: BiometricStateService; + biometricsService: BiometricsService; stateEventRunnerService: StateEventRunnerService; ssoLoginService: SsoLoginServiceAbstraction; billingAccountProfileStateService: BillingAccountProfileStateService; @@ -338,6 +360,8 @@ export default class MainBackground { kdfConfigService: kdfConfigServiceAbstraction; offscreenDocumentService: OffscreenDocumentService; syncServiceListener: SyncServiceListener; + themeStateService: DefaultThemeStateService; + autoSubmitLoginBackground: AutoSubmitLoginBackground; onUpdatedRan: boolean; onReplacedRan: boolean; @@ -347,7 +371,8 @@ export default class MainBackground { private contextMenusBackground: ContextMenusBackground; private idleBackground: IdleBackground; private notificationBackground: NotificationBackground; - private overlayBackground: OverlayBackground; + private overlayBackground: OverlayBackgroundInterface; + private overlayNotificationsBackground: OverlayNotificationsBackgroundInterface; private filelessImporterBackground: FilelessImporterBackground; private runtimeBackground: RuntimeBackground; private tabsBackground: TabsBackground; @@ -357,6 +382,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) => { @@ -376,6 +403,8 @@ export default class MainBackground { const logoutCallback = async (logoutReason: LogoutReason, userId?: UserId) => await this.logout(logoutReason, userId); + const runtimeNativeMessagingBackground = () => this.nativeMessagingBackground; + const refreshAccessTokenErrorCallback = () => { // Send toast to popup this.messagingService.send("showToast", { @@ -410,7 +439,6 @@ export default class MainBackground { this.platformUtilsService = new BackgroundPlatformUtilsService( this.messagingService, (clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs), - async () => this.biometricUnlock(), self, this.offscreenDocumentService, ); @@ -442,6 +470,9 @@ export default class MainBackground { return new ForegroundMemoryStorageService(); } + // For local backed session storage, we expect that the encrypted data on disk will persist longer than the encryption key in memory + // and failures to decrypt because of that are completely expected. For this reason, we pass in `false` to the `EncryptServiceImplementation` + // so that MAC failures are not logged. return new LocalBackedSessionStorageService( sessionKey, this.storageService, @@ -452,27 +483,40 @@ export default class MainBackground { }; this.secureStorageService = this.storageService; // secure storage is not supported in browsers, so we use local storage and warn users when it is used - this.memoryStorageForStateProviders = BrowserApi.isManifestVersion(3) - ? new BrowserMemoryStorageService() // mv3 stores to storage.session - : popupOnlyContext - ? new ForegroundMemoryStorageService() - : new BackgroundMemoryStorageService(); // mv2 stores to memory - this.memoryStorageService = BrowserApi.isManifestVersion(3) - ? this.memoryStorageForStateProviders // manifest v3 can reuse the same storage. They are split for v2 due to lacking a good sync mechanism, which isn't true for v3 - : popupOnlyContext - ? new ForegroundMemoryStorageService() - : new BackgroundMemoryStorageService(); + + if (BrowserApi.isManifestVersion(3)) { + // manifest v3 can reuse the same storage. They are split for v2 due to lacking a good sync mechanism, which isn't true for v3 + this.memoryStorageForStateProviders = new BrowserMemoryStorageService(); // mv3 stores to storage.session + this.memoryStorageService = this.memoryStorageForStateProviders; + } else { + if (popupOnlyContext) { + this.memoryStorageForStateProviders = new ForegroundMemoryStorageService(); + this.memoryStorageService = new ForegroundMemoryStorageService(); + } else { + this.memoryStorageForStateProviders = new BackgroundMemoryStorageService(); // mv2 stores to memory + this.memoryStorageService = this.memoryStorageForStateProviders; + } + } + this.largeObjectMemoryStorageForStateProviders = BrowserApi.isManifestVersion(3) ? 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(storageServiceProvider); + this.globalStateProvider = new DefaultGlobalStateProvider( + storageServiceProvider, + this.logService, + ); const stateEventRegistrarService = new StateEventRegistrarService( this.globalStateProvider, @@ -484,18 +528,18 @@ export default class MainBackground { storageServiceProvider, ); - this.encryptService = - flagEnabled("multithreadDecryption") && BrowserApi.isManifestVersion(2) - ? new MultithreadEncryptServiceImplementation( - this.cryptoFunctionService, - this.logService, - true, - ) - : new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true); + this.encryptService = BrowserApi.isManifestVersion(2) + ? new MultithreadEncryptServiceImplementation( + this.cryptoFunctionService, + this.logService, + true, + ) + : new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true); this.singleUserStateProvider = new DefaultSingleUserStateProvider( storageServiceProvider, stateEventRegistrarService, + this.logService, ); this.accountService = new AccountServiceImplementation( this.messagingService, @@ -513,6 +557,14 @@ export default class MainBackground { this.globalStateProvider, this.derivedStateProvider, ); + + this.taskSchedulerService = this.popupOnlyContext + ? new ForegroundTaskSchedulerService(this.logService, this.stateProvider) + : new BackgroundTaskSchedulerService(this.logService, this.stateProvider); + this.taskSchedulerService.registerTaskHandler(ScheduledTaskNames.scheduleNextSyncInterval, () => + this.fullSync(), + ); + this.environmentService = new BrowserEnvironmentService( this.logService, this.stateProvider, @@ -533,6 +585,11 @@ export default class MainBackground { logoutCallback, ); + this.popupViewCacheBackgroundService = new PopupViewCacheBackgroundService( + messageListener, + this.globalStateProvider, + ); + const migrationRunner = new MigrationRunner( this.storageService, this.logService, @@ -540,7 +597,7 @@ export default class MainBackground { ClientType.Browser, ); - this.stateService = new DefaultBrowserStateService( + this.stateService = new StateService( this.storageService, this.secureStorageService, this.memoryStorageService, @@ -552,7 +609,7 @@ export default class MainBackground { migrationRunner, ); - const themeStateService = new DefaultThemeStateService(this.globalStateProvider); + this.themeStateService = new DefaultThemeStateService(this.globalStateProvider); this.masterPasswordService = new MasterPasswordService( this.stateProvider, @@ -563,6 +620,10 @@ export default class MainBackground { this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); + this.biometricsService = new BackgroundBrowserBiometricsService( + runtimeNativeMessagingBackground, + ); + this.kdfConfigService = new KdfConfigService(this.stateProvider); this.pinService = new PinService( @@ -589,10 +650,11 @@ export default class MainBackground { this.accountService, this.stateProvider, this.biometricStateService, + this.biometricsService, this.kdfConfigService, ); - this.appIdService = new AppIdService(this.globalStateProvider); + this.appIdService = new AppIdService(this.storageService, this.logService); this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); this.organizationService = new OrganizationService(this.stateProvider); @@ -681,6 +743,7 @@ export default class MainBackground { this.secureStorageService, this.userDecryptionOptionsService, this.logService, + this.configService, ); this.devicesService = new DevicesServiceImplementation(this.devicesApiService); @@ -718,8 +781,11 @@ export default class MainBackground { this.environmentService, this.logService, this.stateProvider, + this.authService, ); + this.bulkEncryptService = new FallbackBulkEncryptService(this.encryptService); + this.cipherService = new CipherService( this.cryptoService, this.domainSettingsService, @@ -729,6 +795,7 @@ export default class MainBackground { this.stateService, this.autofillSettingsService, this.encryptService, + this.bulkEncryptService, this.cipherFileUploadService, this.configService, this.stateProvider, @@ -780,6 +847,8 @@ export default class MainBackground { this.authService, this.vaultTimeoutSettingsService, this.stateEventRunnerService, + this.taskSchedulerService, + this.logService, lockedCallback, logoutCallback, ); @@ -818,6 +887,7 @@ export default class MainBackground { this.sendService, this.sendApiService, messageListener, + this.stateProvider, ); } else { this.syncService = new DefaultSyncService( @@ -845,6 +915,7 @@ export default class MainBackground { this.billingAccountProfileStateService, this.tokenService, this.authService, + this.stateProvider, ); this.syncServiceListener = new SyncServiceListener( @@ -859,6 +930,7 @@ export default class MainBackground { this.stateProvider, this.logService, this.authService, + this.taskSchedulerService, ); this.eventCollectionService = new EventCollectionService( this.cipherService, @@ -886,6 +958,8 @@ export default class MainBackground { this.scriptInjectorService, this.accountService, this.authService, + this.configService, + this.userNotificationSettingsService, messageListener, ); this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); @@ -900,6 +974,7 @@ export default class MainBackground { this.collectionService, this.cryptoService, this.pinService, + this.accountService, ); this.individualVaultExportService = new IndividualVaultExportService( @@ -919,6 +994,7 @@ export default class MainBackground { this.cryptoFunctionService, this.collectionService, this.kdfConfigService, + this.accountService, ); this.exportService = new VaultExportService( @@ -936,6 +1012,7 @@ export default class MainBackground { this.stateService, this.authService, this.messagingService, + this.taskSchedulerService, ); this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.authService); @@ -943,24 +1020,24 @@ export default class MainBackground { this.cipherService, this.fido2UserInterfaceService, this.syncService, + this.accountService, this.logService, ); + this.fido2ActiveRequestManager = new Fido2ActiveRequestManager(); this.fido2ClientService = new Fido2ClientService( this.fido2AuthenticatorService, this.configService, this.authService, this.vaultSettingsService, this.domainSettingsService, + this.taskSchedulerService, + this.fido2ActiveRequestManager, this.logService, ); - const systemUtilsServiceReloadCallback = () => { - const forceWindowReload = - this.platformUtilsService.isSafari() || - this.platformUtilsService.isFirefox() || - this.platformUtilsService.isOpera(); - BrowserApi.reloadExtension(forceWindowReload ? self : null); - return Promise.resolve(); + const systemUtilsServiceReloadCallback = async () => { + await this.taskSchedulerService.clearAllScheduledTasks(); + BrowserApi.reloadExtension(); }; this.systemService = new SystemService( @@ -968,11 +1045,11 @@ export default class MainBackground { this.messagingService, this.platformUtilsService, systemUtilsServiceReloadCallback, - this.stateService, this.autofillSettingsService, this.vaultTimeoutSettingsService, this.biometricStateService, this.accountService, + this.taskSchedulerService, ); // Other fields @@ -982,9 +1059,11 @@ export default class MainBackground { if (!this.popupOnlyContext) { this.fido2Background = new Fido2Background( this.logService, + this.fido2ActiveRequestManager, this.fido2ClientService, this.vaultSettingsService, this.scriptInjectorService, + this.configService, ); this.runtimeBackground = new RuntimeBackground( this, @@ -1002,18 +1081,16 @@ export default class MainBackground { this.accountService, ); this.nativeMessagingBackground = new NativeMessagingBackground( - this.accountService, - this.masterPasswordService, this.cryptoService, this.cryptoFunctionService, this.runtimeBackground, this.messagingService, this.appIdService, this.platformUtilsService, - this.stateService, this.logService, this.authService, this.biometricStateService, + this.accountService, ); this.commandsBackground = new CommandsBackground( this, @@ -1028,26 +1105,21 @@ export default class MainBackground { this.authService, this.policyService, this.folderService, - this.stateService, this.userNotificationSettingsService, this.domainSettingsService, this.environmentService, this.logService, - themeStateService, + this.themeStateService, this.configService, + this.accountService, ); - this.overlayBackground = new OverlayBackground( - this.cipherService, - this.autofillService, - this.authService, - this.environmentService, - this.domainSettingsService, - this.stateService, - this.autofillSettingsService, - this.i18nService, - this.platformUtilsService, - themeStateService, + + this.overlayNotificationsBackground = new OverlayNotificationsBackground( + this.logService, + this.configService, + this.notificationBackground, ); + this.filelessImporterBackground = new FilelessImporterBackground( this.configService, this.authService, @@ -1057,10 +1129,15 @@ export default class MainBackground { this.syncService, this.scriptInjectorService, ); - this.tabsBackground = new TabsBackground( - this, - this.notificationBackground, - this.overlayBackground, + + this.autoSubmitLoginBackground = new AutoSubmitLoginBackground( + this.logService, + this.autofillService, + this.scriptInjectorService, + this.authService, + this.configService, + this.platformUtilsService, + this.policyService, ); const contextMenuClickedHandler = new ContextMenuClickedHandler( @@ -1100,7 +1177,6 @@ export default class MainBackground { this.idleBackground = new IdleBackground( this.vaultTimeoutService, - this.stateService, this.notificationsService, this.accountService, this.vaultTimeoutSettingsService, @@ -1168,6 +1244,8 @@ export default class MainBackground { await (this.i18nService as I18nService).init(); (this.eventUploadService as EventUploadService).init(true); + this.popupViewCacheBackgroundService.startObservingTabChanges(); + if (this.popupOnlyContext) { return; } @@ -1176,20 +1254,48 @@ export default class MainBackground { this.fido2Background.init(); await this.runtimeBackground.init(); await this.notificationBackground.init(); + this.overlayNotificationsBackground.init(); this.filelessImporterBackground.init(); - await this.commandsBackground.init(); - await this.overlayBackground.init(); - await this.tabsBackground.init(); + this.commandsBackground.init(); this.contextMenusBackground?.init(); - await this.idleBackground.init(); + this.idleBackground.init(); this.webRequestBackground?.startListening(); this.syncServiceListener?.listener$().subscribe(); + await this.autoSubmitLoginBackground.init(); + + if ( + BrowserApi.isManifestVersion(2) && + (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) + ) { + await this.bulkEncryptService.setFeatureFlagEncryptService( + new BulkEncryptServiceImplementation(this.cryptoFunctionService, this.logService), + ); + } + + // If the user is logged out, switch to the next account + const active = await firstValueFrom(this.accountService.activeAccount$); + if (active != null) { + const authStatus = await firstValueFrom( + this.authService.authStatuses$.pipe(map((statuses) => statuses[active.id])), + ); + if (authStatus === AuthenticationStatus.LoggedOut) { + const nextUpAccount = await firstValueFrom(this.accountService.nextUpAccount$); + await this.switchAccount(nextUpAccount?.id); + } + } + + await this.initOverlayAndTabsBackground(); return new Promise((resolve) => { setTimeout(async () => { await this.refreshBadge(); await this.fullSync(true); + this.taskSchedulerService.setInterval( + ScheduledTaskNames.scheduleNextSyncInterval, + 5 * 60 * 1000, // check every 5 minutes + ); setTimeout(() => this.notificationsService.init(), 2500); + await this.taskSchedulerService.verifyAlarmsState(); resolve(); }, 500); }); @@ -1221,17 +1327,19 @@ export default class MainBackground { } } + async updateOverlayCiphers() { + // overlayBackground null in popup only contexts + if (this.overlayBackground) { + await this.overlayBackground.updateOverlayCiphers(); + } + } + /** * Switch accounts to indicated userId -- null is no active user */ async switchAccount(userId: UserId) { let nextAccountStatus: AuthenticationStatus; try { - const currentlyActiveAccount = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((account) => account?.id)), - ); - // can be removed once password generation history is migrated to state providers - await this.stateService.clearDecryptedData(currentlyActiveAccount); // HACK to ensure account is switched before proceeding const switchPromise = firstValueFrom( this.accountService.activeAccount$.pipe( @@ -1246,6 +1354,7 @@ export default class MainBackground { }), ), ); + await this.popupViewCacheBackgroundService.clearState(); await this.accountService.switchAccount(userId); await switchPromise; // Clear sequentialized caches @@ -1254,7 +1363,7 @@ export default class MainBackground { if (userId == null) { await this.refreshBadge(); await this.refreshMenu(); - await this.overlayBackground?.updateOverlayCiphers(); // null in popup only contexts + await this.updateOverlayCiphers(); this.messagingService.send("goHome"); return; } @@ -1277,7 +1386,7 @@ export default class MainBackground { this.messagingService.send("unlocked", { userId: userId }); await this.refreshBadge(); await this.refreshMenu(); - await this.overlayBackground?.updateOverlayCiphers(); // null in popup only contexts + await this.updateOverlayCiphers(); await this.syncService.fullSync(false); } } finally { @@ -1327,7 +1436,6 @@ export default class MainBackground { ); await Promise.all([ - this.syncService.setLastSync(new Date(0), userBeingLoggedOut), this.cryptoService.clearKeys(userBeingLoggedOut), this.cipherService.clear(userBeingLoggedOut), this.folderService.clear(userBeingLoggedOut), @@ -1335,6 +1443,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 @@ -1359,7 +1468,14 @@ export default class MainBackground { }); if (needStorageReseed) { - await this.reseedStorage(); + await this.reseedStorage( + await firstValueFrom( + this.configService.userCachedFeatureFlag$( + FeatureFlag.StorageReseedRefactor, + userBeingLoggedOut, + ), + ), + ); } if (BrowserApi.isManifestVersion(3)) { @@ -1414,7 +1530,7 @@ export default class MainBackground { await SafariApp.sendMessageToApp("showPopover", null, true); } - async reseedStorage() { + async reseedStorage(doFillBuffer: boolean) { if ( !this.platformUtilsService.isChrome() && !this.platformUtilsService.isVivaldi() && @@ -1423,15 +1539,10 @@ export default class MainBackground { return; } - const storage = await this.storageService.getAll(); - await this.storageService.clear(); - - for (const key in storage) { - // eslint-disable-next-line - if (!storage.hasOwnProperty(key)) { - continue; - } - await this.storageService.save(key, storage[key]); + if (doFillBuffer) { + await this.storageService.fillBuffer(); + } else { + await this.storageService.reseed(); } } @@ -1441,17 +1552,6 @@ export default class MainBackground { } } - async biometricUnlock(): Promise { - if (this.nativeMessagingBackground == null) { - return false; - } - - const responsePromise = this.nativeMessagingBackground.getResponse(); - await this.nativeMessagingBackground.send({ command: "biometricUnlock" }); - const response = await responsePromise; - return response.response === "unlocked"; - } - private async fullSync(override = false) { const syncInternal = 6 * 60 * 60 * 1000; // 6 hours const lastSync = await this.syncService.getLastSync(); @@ -1463,17 +1563,64 @@ export default class MainBackground { if (override || lastSyncAgo >= syncInternal) { await this.syncService.fullSync(override); - this.scheduleNextSync(); - } else { - this.scheduleNextSync(); } } - private scheduleNextSync() { - if (this.syncTimeout) { - clearTimeout(this.syncTimeout); + /** + * Temporary solution to handle initialization of the overlay background behind a feature flag. + * Will be reverted to instantiation within the constructor once the feature flag is removed. + */ + async initOverlayAndTabsBackground() { + if ( + this.popupOnlyContext || + this.overlayBackground || + this.tabsBackground || + (await firstValueFrom(this.authService.activeAccountStatus$)) === + AuthenticationStatus.LoggedOut + ) { + return; } - this.syncTimeout = setTimeout(async () => await this.fullSync(), 5 * 60 * 1000); // check every 5 minutes + const inlineMenuPositioningImprovementsEnabled = await this.configService.getFeatureFlag( + FeatureFlag.InlineMenuPositioningImprovements, + ); + + if (!inlineMenuPositioningImprovementsEnabled) { + this.overlayBackground = new LegacyOverlayBackground( + this.cipherService, + this.autofillService, + this.authService, + this.environmentService, + this.domainSettingsService, + this.autofillSettingsService, + this.i18nService, + this.platformUtilsService, + this.themeStateService, + ); + } else { + this.overlayBackground = new OverlayBackground( + this.logService, + this.cipherService, + this.autofillService, + this.authService, + this.environmentService, + this.domainSettingsService, + this.autofillSettingsService, + this.i18nService, + this.platformUtilsService, + this.vaultSettingsService, + this.fido2ActiveRequestManager, + this.themeStateService, + ); + } + + this.tabsBackground = new TabsBackground( + this, + this.notificationBackground, + this.overlayBackground, + ); + + await this.overlayBackground.init(); + await this.tabsBackground.init(); } } diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index 534a239a811..8f2cac7915c 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -1,8 +1,7 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -10,19 +9,18 @@ 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"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { UserKey, MasterKey } from "@bitwarden/common/types/key"; +import { UserKey } from "@bitwarden/common/types/key"; 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; @@ -65,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; @@ -73,24 +72,22 @@ export class NativeMessagingBackground { private validatingFingerprint: boolean; constructor( - private accountService: AccountService, - private masterPasswordService: InternalMasterPasswordServiceAbstraction, private cryptoService: CryptoService, private cryptoFunctionService: CryptoFunctionService, private runtimeBackground: RuntimeBackground, private messagingService: MessagingService, private appIdService: AppIdService, private platformUtilsService: PlatformUtilsService, - private stateService: StateService, private logService: LogService, private authService: AuthService, private biometricStateService: BiometricStateService, + private accountService: AccountService, ) { if (chrome?.permissions?.onAdded) { // Reload extension to activate nativeMessaging chrome.permissions.onAdded.addListener((permissions) => { if (permissions.permissions?.includes("nativeMessaging")) { - BrowserApi.reloadExtension(null); + BrowserApi.reloadExtension(); } }); } @@ -139,7 +136,7 @@ export class NativeMessagingBackground { const decrypted = await this.cryptoFunctionService.rsaDecrypt( encrypted, this.privateKey, - EncryptionAlgorithm, + HashAlgorithmForEncryption, ); if (this.validatingFingerprint) { @@ -160,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; @@ -183,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) { @@ -217,22 +207,12 @@ export class NativeMessagingBackground { }); } - showWrongUserDialog() { - this.messagingService.send("showDialog", { - title: { key: "nativeMessagingWrongUserTitle" }, - content: { key: "nativeMessagingWrongUserDesc" }, - 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()) { @@ -252,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, + }); + }; }); } @@ -278,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"); } } @@ -303,35 +284,13 @@ 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 available", "not enabled", "not supported", "not unlocked", "canceled"].includes( + message.response, + ) + ) { + this.rejecter(message.response); + return; } // Check for initial setup of biometric unlock @@ -354,60 +313,38 @@ export class NativeMessagingBackground { const userKey = new SymmetricCryptoKey( Utils.fromB64ToArray(message.userKeyB64), ) as UserKey; - await this.cryptoService.setUserKey(userKey); - } else if (message.keyB64) { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - // Backwards compatibility to support cases in which the user hasn't updated their desktop app - // TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3472) - const encUserKeyPrim = await this.stateService.getEncryptedCryptoSymmetricKey(); - const encUserKey = - encUserKeyPrim != null - ? new EncString(encUserKeyPrim) - : await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId); - if (!encUserKey) { - throw new Error("No encrypted user key found"); - } - const masterKey = new SymmetricCryptoKey( - Utils.fromB64ToArray(message.keyB64), - ) as MasterKey; - const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey( - masterKey, - encUserKey, + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); - await this.masterPasswordService.setMasterKey(masterKey, userId); - await this.cryptoService.setUserKey(userKey); + const isUserKeyValid = await this.cryptoService.validateUserKey( + userKey, + activeUserId, + ); + if (isUserKeyValid) { + await this.cryptoService.setUserKey(userKey, activeUserId); + } else { + this.logService.error("Unable to verify biometric unlocked userkey"); + await this.cryptoService.clearKeys(activeUserId); + this.rejecter("userkey wrong"); + return; + } } else { throw new Error("No key received"); } } 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", - }); - - // Exit early - if (this.resolver) { - this.resolver(message); - } + this.rejecter("userkey wrong"); return; } // Verify key is correct by attempting to decrypt a secret try { - await this.cryptoService.getFingerprint(await this.stateService.getUserId()); + 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.showWrongUserDialog(); - - // Exit early - if (this.resolver) { - this.resolver(message); - } + this.rejecter("userkey wrong"); return; } @@ -417,6 +354,10 @@ export class NativeMessagingBackground { } break; } + case "biometricUnlockAvailable": { + this.resolver(message); + break; + } default: this.logService.error("NativeMessage, got unknown command: " + message.command); break; @@ -431,13 +372,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)); @@ -455,7 +397,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 94e96e2dc89..424449f0b65 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -1,8 +1,8 @@ -import { firstValueFrom, map, mergeMap } from "rxjs"; +import { firstValueFrom, map, mergeMap, of, switchMap } from "rxjs"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; +import { AutofillOverlayVisibility, ExtensionCommand } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -68,6 +68,7 @@ export default class RuntimeBackground { ) => { const messagesWithResponse = [ "biometricUnlock", + "biometricUnlockAvailable", "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag", "getInlineMenuFieldQualificationFeatureFlag", ]; @@ -117,7 +118,7 @@ export default class RuntimeBackground { case "collectPageDetailsResponse": switch (msg.sender) { case "autofiller": - case "autofill_cmd": { + case ExtensionCommand.AutofillCommand: { const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); @@ -130,14 +131,14 @@ export default class RuntimeBackground { details: msg.details, }, ], - msg.sender === "autofill_cmd", + msg.sender === ExtensionCommand.AutofillCommand, ); if (totpCode != null) { this.platformUtilsService.copyToClipboard(totpCode); } break; } - case "autofill_card": { + case ExtensionCommand.AutofillCard: { await this.autofillService.doAutoFillActiveTab( [ { @@ -146,12 +147,12 @@ export default class RuntimeBackground { details: msg.details, }, ], - false, + msg.sender === ExtensionCommand.AutofillCard, CipherType.Card, ); break; } - case "autofill_identity": { + case ExtensionCommand.AutofillIdentity: { await this.autofillService.doAutoFillActiveTab( [ { @@ -160,7 +161,7 @@ export default class RuntimeBackground { details: msg.details, }, ], - false, + msg.sender === ExtensionCommand.AutofillIdentity, CipherType.Identity, ); break; @@ -179,7 +180,11 @@ export default class RuntimeBackground { } break; case "biometricUnlock": { - const result = await this.main.biometricUnlock(); + const result = await this.main.biometricsService.authenticateBiometric(); + return result; + } + case "biometricUnlockAvailable": { + const result = await this.main.biometricsService.isBiometricUnlockAvailable(); return result; } case "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag": { @@ -200,6 +205,7 @@ export default class RuntimeBackground { let item: LockedVaultPendingNotificationsData; if (msg.command === "loggedIn") { + await this.main.initOverlayAndTabsBackground(); await this.sendBwInstalledMessageToVault(); await this.autofillService.reloadAutofillScripts(); } @@ -228,11 +234,17 @@ export default class RuntimeBackground { await this.main.refreshBadge(); await this.main.refreshMenu(false); + if (await this.configService.getFeatureFlag(FeatureFlag.ExtensionRefresh)) { + await this.autofillService.setAutoFillOnPageLoadOrgPolicy(); + } break; } case "addToLockedVaultPendingNotifications": this.lockedVaultPendingNotifications.push(msg.data); break; + case "lockVault": + await this.main.vaultTimeoutService.lock(msg.userId); + break; case "logout": await this.main.logout(msg.expired, msg.userId); break; @@ -243,6 +255,11 @@ export default class RuntimeBackground { await this.main.refreshMenu(); }, 2000); await this.configService.ensureConfigFetched(); + await this.main.updateOverlayCiphers(); + + if (await this.configService.getFeatureFlag(FeatureFlag.ExtensionRefresh)) { + await this.autofillService.setAutoFillOnPageLoadOrgPolicy(); + } } break; case "openPopup": @@ -255,9 +272,25 @@ export default class RuntimeBackground { await this.main.refreshBadge(); await this.main.refreshMenu(); break; - case "bgReseedStorage": - await this.main.reseedStorage(); + case "bgReseedStorage": { + const doFillBuffer = await firstValueFrom( + this.accountService.activeAccount$.pipe( + switchMap((account) => { + if (account == null) { + return of(false); + } + + return this.configService.userCachedFeatureFlag$( + FeatureFlag.StorageReseedRefactor, + account.id, + ); + }), + ), + ); + + await this.main.reseedStorage(doFillBuffer); break; + } case "authResult": { const env = await firstValueFrom(this.environmentService.environment$); const vaultUrl = env.getWebVaultUrl(); diff --git a/apps/browser/src/billing/popup/settings/premium-v2.component.html b/apps/browser/src/billing/popup/settings/premium-v2.component.html new file mode 100644 index 00000000000..f578de8ae7a --- /dev/null +++ b/apps/browser/src/billing/popup/settings/premium-v2.component.html @@ -0,0 +1,60 @@ + + + + + + + +
+

{{ "premiumFeatures" | i18n }}

+ + +
+
    +
  • + {{ "ppremiumSignUpStorage" | i18n }} +
  • +
  • + {{ "premiumSignUpTwoStepOptions" | i18n }} +
  • +
  • + {{ "premiumSignUpEmergency" | i18n }} +
  • +
  • + {{ "ppremiumSignUpReports" | i18n }} +
  • +
  • + {{ "ppremiumSignUpTotp" | i18n }} +
  • +
  • + {{ "ppremiumSignUpSupport" | i18n }} +
  • +
  • + {{ "ppremiumSignUpFuture" | i18n }} +
  • +
+
+

{{ priceString }}

+
+
+ + +
+
diff --git a/apps/browser/src/billing/popup/settings/premium-v2.component.ts b/apps/browser/src/billing/popup/settings/premium-v2.component.ts new file mode 100644 index 00000000000..ef4c39942a2 --- /dev/null +++ b/apps/browser/src/billing/popup/settings/premium-v2.component.ts @@ -0,0 +1,86 @@ +import { CommonModule, CurrencyPipe, Location } from "@angular/common"; +import { Component } from "@angular/core"; +import { RouterModule } from "@angular/router"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { PremiumComponent as BasePremiumComponent } from "@bitwarden/angular/vault/components/premium.component"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { + ButtonModule, + CardComponent, + DialogService, + ItemModule, + SectionComponent, +} from "@bitwarden/components"; + +import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component"; +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({ + selector: "app-premium", + templateUrl: "premium-v2.component.html", + standalone: true, + imports: [ + ButtonModule, + CardComponent, + CommonModule, + CurrentAccountComponent, + ItemModule, + JslibModule, + PopupPageComponent, + PopupHeaderComponent, + PopOutComponent, + RouterModule, + SectionComponent, + ], +}) +export class PremiumV2Component extends BasePremiumComponent { + priceString: string; + + constructor( + i18nService: I18nService, + platformUtilsService: PlatformUtilsService, + apiService: ApiService, + configService: ConfigService, + logService: LogService, + private location: Location, + private currencyPipe: CurrencyPipe, + dialogService: DialogService, + environmentService: EnvironmentService, + billingAccountProfileStateService: BillingAccountProfileStateService, + ) { + super( + i18nService, + platformUtilsService, + apiService, + configService, + logService, + dialogService, + environmentService, + billingAccountProfileStateService, + ); + + // Support old price string. Can be removed in future once all translations are properly updated. + const thePrice = this.currencyPipe.transform(this.price, "$"); + // Safari extension crashes due to $1 appearing in the price string ($10.00). Escape the $ to fix. + const formattedPrice = this.platformUtilsService.isSafari() + ? thePrice.replace("$", "$$$") + : thePrice; + this.priceString = i18nService.t("premiumPriceV2", formattedPrice); + if (this.priceString.indexOf("%price%") > -1) { + this.priceString = this.priceString.replace("%price%", thePrice); + } + } + + goBack() { + this.location.back(); + } +} diff --git a/apps/browser/src/billing/popup/settings/premium.component.ts b/apps/browser/src/billing/popup/settings/premium.component.ts index dcfbc84aec8..ed64574d17d 100644 --- a/apps/browser/src/billing/popup/settings/premium.component.ts +++ b/apps/browser/src/billing/popup/settings/premium.component.ts @@ -4,11 +4,11 @@ import { Component } from "@angular/core"; import { PremiumComponent as BasePremiumComponent } from "@bitwarden/angular/vault/components/premium.component"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { DialogService } from "@bitwarden/components"; @Component({ @@ -22,7 +22,7 @@ export class PremiumComponent extends BasePremiumComponent { i18nService: I18nService, platformUtilsService: PlatformUtilsService, apiService: ApiService, - stateService: StateService, + configService: ConfigService, logService: LogService, private location: Location, private currencyPipe: CurrencyPipe, @@ -34,8 +34,8 @@ export class PremiumComponent extends BasePremiumComponent { i18nService, platformUtilsService, apiService, + configService, logService, - stateService, dialogService, environmentService, billingAccountProfileStateService, diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index b1c51911ec8..2d7f46fa59a 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.0", + "version": "2024.9.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", @@ -59,6 +59,7 @@ "clipboardRead", "clipboardWrite", "idle", + "alarms", "webRequest", "webRequestBlocking", "webNavigation" @@ -66,7 +67,12 @@ "optional_permissions": ["nativeMessaging", "privacy"], "content_security_policy": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'", "sandbox": { - "pages": ["overlay/button.html", "overlay/list.html"], + "pages": [ + "overlay/menu-button.html", + "overlay/menu-list.html", + "overlay/button.html", + "overlay/list.html" + ], "content_security_policy": "sandbox allow-scripts; script-src 'self'" }, "commands": { @@ -88,7 +94,13 @@ "suggested_key": { "default": "Ctrl+Shift+L" }, - "description": "__MSG_commandAutofillDesc__" + "description": "__MSG_commandAutofillLoginDesc__" + }, + "autofill_card": { + "description": "__MSG_commandAutofillCardDesc__" + }, + "autofill_identity": { + "description": "__MSG_commandAutofillIdentityDesc__" }, "generate_password": { "suggested_key": { @@ -106,6 +118,9 @@ "notification/bar.html", "images/icon38.png", "images/icon38_locked.png", + "overlay/menu-button.html", + "overlay/menu-list.html", + "overlay/menu.html", "overlay/button.html", "overlay/list.html", "popup/fonts/*" @@ -119,7 +134,9 @@ "sidebar_action": { "default_title": "Bitwarden", "default_panel": "popup/index.html?uilocation=sidebar", - "default_icon": "images/icon19.png" + "default_icon": "images/icon19.png", + "open_at_install": false, + "browser_style": false }, "storage": { "managed_schema": "managed_schema.json" diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index 40060a7fd93..5e132774e6e 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.0", + "version": "2024.9.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", @@ -59,6 +59,7 @@ "clipboardRead", "clipboardWrite", "idle", + "alarms", "scripting", "offscreen", "webRequest", @@ -72,7 +73,12 @@ "sandbox": "sandbox allow-scripts; script-src 'self'" }, "sandbox": { - "pages": ["overlay/button.html", "overlay/list.html"] + "pages": [ + "overlay/menu-button.html", + "overlay/menu-list.html", + "overlay/button.html", + "overlay/list.html" + ] }, "commands": { "_execute_action": { @@ -93,7 +99,13 @@ "suggested_key": { "default": "Ctrl+Shift+L" }, - "description": "__MSG_commandAutofillDesc__" + "description": "__MSG_commandAutofillLoginDesc__" + }, + "autofill_card": { + "description": "__MSG_commandAutofillCardDesc__" + }, + "autofill_identity": { + "description": "__MSG_commandAutofillIdentityDesc__" }, "generate_password": { "suggested_key": { @@ -112,6 +124,9 @@ "notification/bar.html", "images/icon38.png", "images/icon38_locked.png", + "overlay/menu-button.html", + "overlay/menu-list.html", + "overlay/menu.html", "overlay/button.html", "overlay/list.html", "popup/fonts/*" @@ -128,7 +143,8 @@ "sidebar_action": { "default_title": "Bitwarden", "default_panel": "popup/index.html?uilocation=sidebar", - "default_icon": "images/icon19.png" + "default_icon": "images/icon19.png", + "open_at_install": false }, "storage": { "managed_schema": "managed_schema.json" diff --git a/apps/browser/src/models/account.ts b/apps/browser/src/models/account.ts deleted file mode 100644 index 519f1bda6bb..00000000000 --- a/apps/browser/src/models/account.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Jsonify } from "type-fest"; - -import { Account as BaseAccount } from "@bitwarden/common/platform/models/domain/account"; - -import { BrowserComponentState } from "./browserComponentState"; -import { BrowserGroupingsComponentState } from "./browserGroupingsComponentState"; -import { BrowserSendComponentState } from "./browserSendComponentState"; - -export class Account extends BaseAccount { - groupings?: BrowserGroupingsComponentState; - send?: BrowserSendComponentState; - ciphers?: BrowserComponentState; - sendType?: BrowserComponentState; - - constructor(init: Partial) { - super(init); - - this.groupings = init?.groupings ?? new BrowserGroupingsComponentState(); - this.send = init?.send ?? new BrowserSendComponentState(); - this.ciphers = init?.ciphers ?? new BrowserComponentState(); - this.sendType = init?.sendType ?? new BrowserComponentState(); - } - - static fromJSON(json: Jsonify): Account { - if (json == null) { - return null; - } - - return Object.assign(new Account({}), json, super.fromJSON(json), { - groupings: BrowserGroupingsComponentState.fromJSON(json.groupings), - send: BrowserSendComponentState.fromJSON(json.send), - ciphers: BrowserComponentState.fromJSON(json.ciphers), - sendType: BrowserComponentState.fromJSON(json.sendType), - }); - } -} diff --git a/apps/browser/src/models/biometricErrors.ts b/apps/browser/src/models/biometricErrors.ts index 822a5c16f42..42d9c679d34 100644 --- a/apps/browser/src/models/biometricErrors.ts +++ b/apps/browser/src/models/biometricErrors.ts @@ -3,7 +3,16 @@ type BiometricError = { description: string; }; -export type BiometricErrorTypes = "startDesktop" | "desktopIntegrationDisabled"; +export type BiometricErrorTypes = + | "startDesktop" + | "desktopIntegrationDisabled" + | "not enabled" + | "not supported" + | "not unlocked" + | "invalidateEncryption" + | "userkey wrong" + | "wrongUserId" + | "not available"; export const BiometricErrors: Record = { startDesktop: { @@ -14,4 +23,32 @@ 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", + }, + "not available": { + title: "biometricsNotAvailableTitle", + description: "biometricsNotAvailableDesc", + }, }; diff --git a/apps/browser/src/platform/alarms/alarm-state.ts b/apps/browser/src/platform/alarms/alarm-state.ts deleted file mode 100644 index fa18e26ed1c..00000000000 --- a/apps/browser/src/platform/alarms/alarm-state.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { clearClipboardAlarmName } from "../../autofill/clipboard"; -import { BrowserApi } from "../browser/browser-api"; - -export const alarmKeys = [clearClipboardAlarmName] as const; -export type AlarmKeys = (typeof alarmKeys)[number]; - -type AlarmState = { [T in AlarmKeys]: number | undefined }; - -const alarmState: AlarmState = { - clearClipboard: null, - //TODO once implemented vaultTimeout: null; - //TODO once implemented checkNotifications: null; - //TODO once implemented (if necessary) processReload: null; -}; - -/** - * Retrieves the set alarm time (planned execution) for a give an commandName {@link AlarmState} - * @param commandName A command that has been previously registered with {@link AlarmState} - * @returns {Promise} null or Unix epoch timestamp when the alarm action is supposed to execute - * @example - * // getAlarmTime(clearClipboard) - */ -export async function getAlarmTime(commandName: AlarmKeys): Promise { - let alarmTime: number; - if (BrowserApi.isManifestVersion(3)) { - const fromSessionStore = await chrome.storage.session.get(commandName); - alarmTime = fromSessionStore[commandName]; - } else { - alarmTime = alarmState[commandName]; - } - - return alarmTime; -} - -/** - * Registers an action that should execute after the given time has passed - * @param commandName A command that has been previously registered with {@link AlarmState} - * @param delay_ms The number of ms from now in which the command should execute from - * @example - * // setAlarmTime(clearClipboard, 5000) register the clearClipboard action which will execute when at least 5 seconds from now have passed - */ -export async function setAlarmTime(commandName: AlarmKeys, delay_ms: number): Promise { - if (!delay_ms || delay_ms === 0) { - await this.clearAlarmTime(commandName); - return; - } - - const time = Date.now() + delay_ms; - await setAlarmTimeInternal(commandName, time); -} - -/** - * Clears the time currently set for a given command - * @param commandName A command that has been previously registered with {@link AlarmState} - */ -export async function clearAlarmTime(commandName: AlarmKeys): Promise { - await setAlarmTimeInternal(commandName, null); -} - -async function setAlarmTimeInternal(commandName: AlarmKeys, time: number): Promise { - if (BrowserApi.isManifestVersion(3)) { - await chrome.storage.session.set({ [commandName]: time }); - } else { - alarmState[commandName] = time; - } -} diff --git a/apps/browser/src/platform/alarms/on-alarm-listener.ts b/apps/browser/src/platform/alarms/on-alarm-listener.ts deleted file mode 100644 index 274f19f7897..00000000000 --- a/apps/browser/src/platform/alarms/on-alarm-listener.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ClearClipboard, clearClipboardAlarmName } from "../../autofill/clipboard"; - -import { alarmKeys, clearAlarmTime, getAlarmTime } from "./alarm-state"; - -export const onAlarmListener = async (alarm: chrome.alarms.Alarm) => { - alarmKeys.forEach(async (key) => { - const executionTime = await getAlarmTime(key); - if (!executionTime) { - return; - } - - const currentDate = Date.now(); - if (executionTime > currentDate) { - return; - } - - await clearAlarmTime(key); - - switch (key) { - case clearClipboardAlarmName: - // 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 - ClearClipboard.run(); - break; - default: - } - }); -}; diff --git a/apps/browser/src/platform/alarms/register-alarms.ts b/apps/browser/src/platform/alarms/register-alarms.ts deleted file mode 100644 index 86b9fb97747..00000000000 --- a/apps/browser/src/platform/alarms/register-alarms.ts +++ /dev/null @@ -1,31 +0,0 @@ -const NUMBER_OF_ALARMS = 6; - -export function registerAlarms() { - alarmsToBeCreated(NUMBER_OF_ALARMS); -} - -/** - * Creates staggered alarms that periodically (1min) raise OnAlarm events. The staggering is calculated based on the number of alarms passed in. - * @param numberOfAlarms Number of named alarms, that shall be registered - * @example - * // alarmsToBeCreated(2) results in 2 alarms separated by 30 seconds - * @example - * // alarmsToBeCreated(4) results in 4 alarms separated by 15 seconds - * @example - * // alarmsToBeCreated(6) results in 6 alarms separated by 10 seconds - * @example - * // alarmsToBeCreated(60) results in 60 alarms separated by 1 second - */ -function alarmsToBeCreated(numberOfAlarms: number): void { - const oneMinuteInMs = 60 * 1000; - const offset = oneMinuteInMs / numberOfAlarms; - - let calculatedWhen: number = Date.now() + offset; - - for (let index = 0; index < numberOfAlarms; index++) { - // 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 - chrome.alarms.create(`bw_alarm${index}`, { periodInMinutes: 1, when: calculatedWhen }); - calculatedWhen += offset; - } -} diff --git a/apps/browser/src/platform/background.html b/apps/browser/src/platform/background.html index 0cd95f3f020..dd5767ab209 100644 --- a/apps/browser/src/platform/background.html +++ b/apps/browser/src/platform/background.html @@ -1,4 +1,5 @@ - + + diff --git a/apps/browser/src/platform/browser/browser-api.spec.ts b/apps/browser/src/platform/browser/browser-api.spec.ts index adf248707c2..6e8a0f3002d 100644 --- a/apps/browser/src/platform/browser/browser-api.spec.ts +++ b/apps/browser/src/platform/browser/browser-api.spec.ts @@ -276,26 +276,8 @@ describe("BrowserApi", () => { }); describe("reloadExtension", () => { - it("reloads the window location if the passed globalContext is for the window", () => { - const windowMock = mock({ - location: { reload: jest.fn() }, - }) as unknown as Window & typeof globalThis; - - BrowserApi.reloadExtension(windowMock); - - expect(windowMock.location.reload).toHaveBeenCalled(); - }); - - it("reloads the extension runtime if the passed globalContext is not for the window", () => { - const globalMock = mock({}) as any; - BrowserApi.reloadExtension(globalMock); - - expect(chrome.runtime.reload).toHaveBeenCalled(); - }); - - it("reloads the extension runtime if a null value is passed as the globalContext", () => { - BrowserApi.reloadExtension(null); - + it("forwards call to extension runtime", () => { + BrowserApi.reloadExtension(); expect(chrome.runtime.reload).toHaveBeenCalled(); }); }); @@ -424,7 +406,6 @@ describe("BrowserApi", () => { target: { tabId: tabId, allFrames: injectDetails.allFrames, - frameIds: null, }, files: [injectDetails.file], injectImmediately: true, @@ -450,7 +431,6 @@ describe("BrowserApi", () => { expect(chrome.scripting.executeScript).toHaveBeenCalledWith({ target: { tabId: tabId, - allFrames: injectDetails.allFrames, frameIds: [frameId], }, files: [injectDetails.file], @@ -477,7 +457,6 @@ describe("BrowserApi", () => { target: { tabId: tabId, allFrames: injectDetails.allFrames, - frameIds: null, }, files: null, injectImmediately: true, diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index 0d461a69830..a5ca7a65ff4 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -412,18 +412,9 @@ export class BrowserApi { } /** - * Handles reloading the extension, either by calling the window location - * to reload or by calling the extension's runtime to reload. - * - * @param globalContext - The global context to use for the reload. + * Handles reloading the extension using the underlying functionality exposed by the browser API. */ - static reloadExtension(globalContext: (Window & typeof globalThis) | null) { - // The passed globalContext might be a ServiceWorkerGlobalScope, as a result - // we need to check if the location object exists before calling reload on it. - if (typeof globalContext?.location?.reload === "function") { - return (globalContext as any).location.reload(true); - } - + static reloadExtension() { return chrome.runtime.reload(); } @@ -522,12 +513,20 @@ export class BrowserApi { }, ): Promise { if (BrowserApi.isManifestVersion(3)) { + const target: chrome.scripting.InjectionTarget = { + tabId, + }; + + if (typeof details.frameId === "number") { + target.frameIds = [details.frameId]; + } + + if (!target.frameIds?.length && details.allFrames) { + target.allFrames = details.allFrames; + } + return chrome.scripting.executeScript({ - target: { - tabId: tabId, - allFrames: details.allFrames, - frameIds: details.frameId ? [details.frameId] : null, - }, + target, files: details.file ? [details.file] : null, injectImmediately: details.runAt === "document_start", world: scriptingApiDetails?.world || "ISOLATED", diff --git a/libs/admin-console/src/index.ts b/apps/browser/src/platform/listeners/on-command-listener.ts similarity index 100% rename from libs/admin-console/src/index.ts rename to apps/browser/src/platform/listeners/on-command-listener.ts 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/browser-popup-utils.ts b/apps/browser/src/platform/popup/browser-popup-utils.ts index a2249d466cb..fb53d3451f2 100644 --- a/apps/browser/src/platform/popup/browser-popup-utils.ts +++ b/apps/browser/src/platform/popup/browser-popup-utils.ts @@ -86,7 +86,7 @@ class BrowserPopupUtils { * Identifies if the background page needs to be initialized. */ static backgroundInitializationRequired() { - return !BrowserApi.getBackgroundPage(); + return !BrowserApi.getBackgroundPage() || BrowserApi.isManifestVersion(3); } /** diff --git a/apps/browser/src/platform/popup/header.component.ts b/apps/browser/src/platform/popup/header.component.ts index 13738378667..cba9f20b629 100644 --- a/apps/browser/src/platform/popup/header.component.ts +++ b/apps/browser/src/platform/popup/header.component.ts @@ -1,14 +1,18 @@ +import { CommonModule } from "@angular/common"; import { Component, Input } from "@angular/core"; import { Observable, map, of, switchMap } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { CurrentAccountComponent } from "../../auth/popup/account-switching/current-account.component"; import { enableAccountSwitching } from "../flags"; @Component({ selector: "app-header", templateUrl: "header.component.html", + standalone: true, + imports: [CommonModule, CurrentAccountComponent], }) export class HeaderComponent { @Input() noTheme = false; diff --git a/apps/browser/src/platform/popup/layout/popup-footer.component.html b/apps/browser/src/platform/popup/layout/popup-footer.component.html index 2cbbca79c0b..777e0ab60da 100644 --- a/apps/browser/src/platform/popup/layout/popup-footer.component.html +++ b/apps/browser/src/platform/popup/layout/popup-footer.component.html @@ -1,9 +1,12 @@
-
+
+
+ +
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 e866ba4e81f..fefc7154314 100644 --- a/apps/browser/src/platform/popup/layout/popup-header.component.html +++ b/apps/browser/src/platform/popup/layout/popup-header.component.html @@ -1,5 +1,11 @@
@@ -8,10 +14,13 @@ type="button" *ngIf="showBackButton" [title]="'back' | i18n" - [ariaLabel]="'back' | i18n" + [attr.aria-label]="'back' | i18n" [bitAction]="backAction" > -

{{ pageTitle }}

+

+ {{ pageTitle }} +

+
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 1b491ea881c..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,6 +1,6 @@ import { BooleanInput, coerceBooleanProperty } from "@angular/cdk/coercion"; -import { CommonModule, Location } from "@angular/common"; -import { Component, Input } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { Component, Input, Signal, inject } from "@angular/core"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { @@ -10,6 +10,10 @@ import { TypographyModule, } from "@bitwarden/components"; +import { PopupRouterCacheService } from "../view-cache/popup-router-cache.service"; + +import { PopupPageComponent } from "./popup-page.component"; + @Component({ selector: "popup-header", templateUrl: "popup-header.component.html", @@ -17,6 +21,13 @@ import { imports: [TypographyModule, CommonModule, IconButtonModule, JslibModule, AsyncActionsModule], }) export class PopupHeaderComponent { + private popupRouterCacheService = inject(PopupRouterCacheService); + protected pageContentScrolled: Signal = inject(PopupPageComponent).isScrolled; + + /** Background color */ + @Input() + background: "default" | "alt" = "default"; + /** Display the back button, which uses Location.back() to go back one page in history */ @Input() get showBackButton() { @@ -38,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.mdx b/apps/browser/src/platform/popup/layout/popup-layout.mdx index 6f72f325bf1..aa11b4099a9 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.mdx +++ b/apps/browser/src/platform/popup/layout/popup-layout.mdx @@ -41,9 +41,20 @@ page looks nice when the extension is popped out. - `footer` - Use the `popup-footer` component. - Not every page will have a footer. +- `above-scroll-area` + - When the page content overflows, this content will be "stuck" to the top of the page upon + scrolling. - default - Whatever content you want in `main`. +**Inputs** + +- `loading` + - When `true`, displays a loading state overlay instead of the default content. Defaults to + `false`. +- `loadingText` + - Custom text to be applied to the loading element for screenreaders only. Defaults to "Loading". + Basic usage example: ```html @@ -63,6 +74,9 @@ Basic usage example: - `showBackButton`: optional, defaults to `false` - Toggles the back button to appear. The back button uses `Location.back()` to navigate back one page in history. +- `background`: optional + - `"default"` uses a white background + - `"alt"` uses a transparent background **Slots** @@ -81,6 +95,12 @@ Usage example: ``` +### Transparent header + + + + + Common interactive elements to insert into the `end` slot are: - `app-current-account`: shows current account and switcher @@ -137,8 +157,20 @@ When the browser extension is popped out, the "popout" button should not be pass +# Other stories + ## Centered Content +An example of how to center the default content. + + +## Loading + +An example of what the loading state looks like. + + + + 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 cc7758d9680..affa804cc79 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -12,8 +12,12 @@ import { IconButtonModule, ItemModule, NoItemsModule, + SearchModule, + 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"; @@ -33,56 +37,41 @@ class ExtensionContainerComponent {} @Component({ selector: "vault-placeholder", template: ` - - - + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + `, standalone: true, - imports: [CommonModule, ItemModule, BadgeModule, IconButtonModule], + imports: [CommonModule, ItemModule, BadgeModule, IconButtonModule, SectionComponent], }) class VaultComponent { protected data = Array.from(Array(20).keys()); } -@Component({ - selector: "generator-placeholder", - template: `
generator stuff here
`, - standalone: true, -}) -class GeneratorComponent {} - -@Component({ - selector: "send-placeholder", - template: `
send some stuff
`, - standalone: true, -}) -class SendComponent {} - -@Component({ - selector: "settings-placeholder", - template: `
change your settings
`, - standalone: true, -}) -class SettingsComponent {} - @Component({ selector: "mock-add-button", template: ` @@ -124,6 +113,18 @@ class MockPopoutButtonComponent {} }) class MockCurrentAccountComponent {} +@Component({ + selector: "mock-search", + template: ` +
+ +
+ `, + standalone: true, + imports: [SearchModule], +}) +class MockSearchComponent {} + @Component({ selector: "mock-vault-page", template: ` @@ -135,6 +136,7 @@ class MockCurrentAccountComponent {} + `, @@ -145,6 +147,7 @@ class MockCurrentAccountComponent {} MockAddButtonComponent, MockPopoutButtonComponent, MockCurrentAccountComponent, + MockSearchComponent, VaultComponent, ], }) @@ -186,7 +189,7 @@ class MockVaultPagePoppedComponent {} - +
Generator content here
`, standalone: true, @@ -196,7 +199,6 @@ class MockVaultPagePoppedComponent {} MockAddButtonComponent, MockPopoutButtonComponent, MockCurrentAccountComponent, - GeneratorComponent, ], }) class MockGeneratorPageComponent {} @@ -212,7 +214,7 @@ class MockGeneratorPageComponent {} - +
Send content here
`, standalone: true, @@ -222,7 +224,6 @@ class MockGeneratorPageComponent {} MockAddButtonComponent, MockPopoutButtonComponent, MockCurrentAccountComponent, - SendComponent, ], }) class MockSendPageComponent {} @@ -238,7 +239,7 @@ class MockSendPageComponent {} - +
Settings content here
`, standalone: true, @@ -248,7 +249,6 @@ class MockSendPageComponent {} MockAddButtonComponent, MockPopoutButtonComponent, MockCurrentAccountComponent, - SettingsComponent, ], }) class MockSettingsPageComponent {} @@ -266,6 +266,7 @@ class MockSettingsPageComponent {} + `, @@ -279,6 +280,7 @@ class MockSettingsPageComponent {} MockPopoutButtonComponent, MockCurrentAccountComponent, VaultComponent, + IconButtonModule, ], }) class MockVaultSubpageComponent {} @@ -303,6 +305,7 @@ export default { MockSettingsPageComponent, MockVaultPagePoppedComponent, NoItemsModule, + VaultComponent, ], providers: [ { @@ -310,6 +313,8 @@ export default { useFactory: () => { return new I18nMockService({ back: "Back", + loading: "Loading", + search: "Search", }); }, }, @@ -331,6 +336,12 @@ export default { { useHash: true }, ), ), + { + provide: PopupRouterCacheService, + useValue: { + back() {}, + } as Partial, + }, ], }), ], @@ -404,3 +415,36 @@ export const CenteredContent: Story = { `, }), }; + +export const Loading: Story = { + render: (args) => ({ + props: args, + template: /* HTML */ ` + + + + + Content would go here + + + + `, + }), +}; + +export const TransparentHeader: Story = { + render: (args) => ({ + props: args, + template: /* HTML */ ` + + + 🤠 Custom Content + + + + + `, + }), +}; diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.html b/apps/browser/src/platform/popup/layout/popup-page.component.html index b3dcd626ae3..8a7bedf0882 100644 --- a/apps/browser/src/platform/popup/layout/popup-page.component.html +++ b/apps/browser/src/platform/popup/layout/popup-page.component.html @@ -1,7 +1,33 @@ -
-
- +
+
+
+
+
+ +
+
+ + +
diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.ts b/apps/browser/src/platform/popup/layout/popup-page.component.ts index 1223a6f4188..7b4665040fb 100644 --- a/apps/browser/src/platform/popup/layout/popup-page.component.ts +++ b/apps/browser/src/platform/popup/layout/popup-page.component.ts @@ -1,11 +1,32 @@ -import { Component } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { booleanAttribute, Component, inject, Input, signal } from "@angular/core"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @Component({ selector: "popup-page", templateUrl: "popup-page.component.html", standalone: true, host: { - class: "tw-h-full tw-flex tw-flex-col tw-flex-1 tw-overflow-y-auto", + class: "tw-h-full tw-flex tw-flex-col tw-flex-1 tw-overflow-y-hidden", }, + imports: [CommonModule], }) -export class PopupPageComponent {} +export class PopupPageComponent { + protected i18nService = inject(I18nService); + + @Input() loading = false; + + @Input({ transform: booleanAttribute }) + disablePadding = false; + + protected scrolled = signal(false); + isScrolled = this.scrolled.asReadonly(); + + /** Accessible loading label for the spinner. Defaults to "loading" */ + @Input() loadingText?: string = this.i18nService.t("loading"); + + handleScroll(event: Event) { + this.scrolled.set((event.currentTarget as HTMLElement).scrollTop !== 0); + } +} 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..8876ac44d59 --- /dev/null +++ b/apps/browser/src/platform/popup/view-cache/popup-router-cache.service.ts @@ -0,0 +1,139 @@ +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, tap } 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); + + private hasNavigated = false; + + constructor() { + // init history with existing state + this.history$() + .pipe(first()) + .subscribe( + (history) => + Array.isArray(history) && history.forEach((location) => this.location.go(location)), + ); + + // update state when route change occurs + this.router.events + .pipe( + filter((event) => event instanceof NavigationEnd), + tap(() => { + // `Location.back()` can now be called successfully + this.hasNavigated = true; + }), + 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) { + 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 ? prevState.slice(0, -1) : [])); + + if (this.hasNavigated) { + this.location.back(); + 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..465a6e6c69c --- /dev/null +++ b/apps/browser/src/platform/popup/view-cache/popup-router-cache.spec.ts @@ -0,0 +1,123 @@ +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("returns true if the history stack is null", async () => { + await service.setHistory(null); + + 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/popup/view-cache/popup-view-cache.service.ts b/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts new file mode 100644 index 00000000000..819a2aa3e46 --- /dev/null +++ b/apps/browser/src/platform/popup/view-cache/popup-view-cache.service.ts @@ -0,0 +1,136 @@ +import { + DestroyRef, + effect, + inject, + Injectable, + Injector, + signal, + WritableSignal, +} from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormGroup } from "@angular/forms"; +import { NavigationEnd, Router } from "@angular/router"; +import { filter, firstValueFrom, skip } from "rxjs"; +import { Jsonify } from "type-fest"; + +import { + FormCacheOptions, + SignalCacheOptions, + ViewCacheService, +} from "@bitwarden/angular/platform/abstractions/view-cache.service"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { MessageSender } from "@bitwarden/common/platform/messaging"; +import { GlobalStateProvider } from "@bitwarden/common/platform/state"; + +import { + ClEAR_VIEW_CACHE_COMMAND, + POPUP_VIEW_CACHE_KEY, + SAVE_VIEW_CACHE_COMMAND, +} from "../../services/popup-view-cache-background.service"; + +/** + * Popup implementation of {@link ViewCacheService}. + * + * Persists user changes between popup open and close + */ +@Injectable({ + providedIn: "root", +}) +export class PopupViewCacheService implements ViewCacheService { + private configService = inject(ConfigService); + private globalStateProvider = inject(GlobalStateProvider); + private messageSender = inject(MessageSender); + private router = inject(Router); + + private featureEnabled: boolean; + + private _cache: Record; + private get cache(): Record { + if (!this._cache) { + throw new Error("Dirty View Cache not initialized"); + } + return this._cache; + } + + /** + * Initialize the service. This should only be called once. + */ + async init() { + this.featureEnabled = await this.configService.getFeatureFlag(FeatureFlag.PersistPopupView); + const initialState = this.featureEnabled + ? await firstValueFrom(this.globalStateProvider.get(POPUP_VIEW_CACHE_KEY).state$) + : {}; + this._cache = Object.freeze(initialState ?? {}); + + this.router.events + .pipe( + filter((e) => e instanceof NavigationEnd), + /** Skip the first navigation triggered by `popupRouterCacheGuard` */ + skip(1), + ) + .subscribe(() => this.clearState()); + } + + /** + * @see {@link ViewCacheService.signal} + */ + signal(options: SignalCacheOptions): WritableSignal { + const { + deserializer = (v: Jsonify): T => v as T, + key, + injector = inject(Injector), + initialValue, + } = options; + const cachedValue = this.cache[key] ? deserializer(JSON.parse(this.cache[key])) : initialValue; + const _signal = signal(cachedValue); + + effect( + () => { + this.updateState(key, JSON.stringify(_signal())); + }, + { injector }, + ); + + return _signal; + } + + /** + * @see {@link ViewCacheService.formGroup} + */ + formGroup(options: FormCacheOptions): TFormGroup { + const { control, injector } = options; + + const _signal = this.signal({ + ...options, + initialValue: control.getRawValue(), + }); + + const value = _signal(); + if (value !== undefined && JSON.stringify(value) !== JSON.stringify(control.getRawValue())) { + control.setValue(value); + control.markAsDirty(); + } + + control.valueChanges.pipe(takeUntilDestroyed(injector?.get(DestroyRef))).subscribe(() => { + _signal.set(control.getRawValue()); + }); + + return control; + } + + private updateState(key: string, value: string) { + if (!this.featureEnabled) { + return; + } + + this.messageSender.send(SAVE_VIEW_CACHE_COMMAND, { + key, + value, + }); + } + + private clearState() { + this.messageSender.send(ClEAR_VIEW_CACHE_COMMAND, {}); + } +} diff --git a/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts b/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts new file mode 100644 index 00000000000..fbe94bece8c --- /dev/null +++ b/apps/browser/src/platform/popup/view-cache/popup-view-cache.spec.ts @@ -0,0 +1,224 @@ +import { Component, inject, Injector } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { FormControl, FormGroup } from "@angular/forms"; +import { Router } from "@angular/router"; +import { RouterTestingModule } from "@angular/router/testing"; +import { MockProxy, mock } from "jest-mock-extended"; + +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { MessageSender } from "@bitwarden/common/platform/messaging"; +import { GlobalStateProvider } from "@bitwarden/common/platform/state"; +import { FakeGlobalState, FakeGlobalStateProvider } from "@bitwarden/common/spec"; + +import { + ClEAR_VIEW_CACHE_COMMAND, + POPUP_VIEW_CACHE_KEY, + SAVE_VIEW_CACHE_COMMAND, +} from "../../services/popup-view-cache-background.service"; + +import { PopupViewCacheService } from "./popup-view-cache.service"; + +@Component({ template: "" }) +export class EmptyComponent {} + +@Component({ template: "" }) +export class TestComponent { + private viewCacheService = inject(PopupViewCacheService); + + formGroup = this.viewCacheService.formGroup({ + key: "test-form-cache", + control: new FormGroup({ + name: new FormControl("initial name"), + }), + }); + + signal = this.viewCacheService.signal({ + key: "test-signal", + initialValue: "initial signal", + }); +} + +describe("popup view cache", () => { + const configServiceMock = mock(); + let testBed: TestBed; + let service: PopupViewCacheService; + let fakeGlobalState: FakeGlobalState>; + let messageSenderMock: MockProxy; + let router: Router; + + const initServiceWithState = async (state: Record) => { + await fakeGlobalState.update(() => state); + await service.init(); + }; + + beforeEach(async () => { + jest.spyOn(configServiceMock, "getFeatureFlag").mockResolvedValue(true); + messageSenderMock = mock(); + + const fakeGlobalStateProvider = new FakeGlobalStateProvider(); + fakeGlobalState = fakeGlobalStateProvider.getFake(POPUP_VIEW_CACHE_KEY); + + testBed = TestBed.configureTestingModule({ + imports: [ + RouterTestingModule.withRoutes([ + { path: "a", component: EmptyComponent }, + { path: "b", component: EmptyComponent }, + ]), + ], + providers: [ + { provide: GlobalStateProvider, useValue: fakeGlobalStateProvider }, + { provide: MessageSender, useValue: messageSenderMock }, + { provide: ConfigService, useValue: configServiceMock }, + ], + }); + + await testBed.compileComponents(); + + router = testBed.inject(Router); + service = testBed.inject(PopupViewCacheService); + }); + + it("should initialize signal when ran within an injection context", async () => { + await initServiceWithState({}); + + const signal = TestBed.runInInjectionContext(() => + service.signal({ + key: "foo-123", + initialValue: "foo", + }), + ); + + expect(signal()).toBe("foo"); + }); + + it("should initialize signal when provided an injector", async () => { + await initServiceWithState({}); + + const injector = TestBed.inject(Injector); + + const signal = service.signal({ + key: "foo-123", + initialValue: "foo", + injector, + }); + + expect(signal()).toBe("foo"); + }); + + it("should initialize signal from state", async () => { + await initServiceWithState({ "foo-123": JSON.stringify("bar") }); + + const injector = TestBed.inject(Injector); + + const signal = service.signal({ + key: "foo-123", + initialValue: "foo", + injector, + }); + + expect(signal()).toBe("bar"); + }); + + it("should initialize form from state", async () => { + await initServiceWithState({ "test-form-cache": JSON.stringify({ name: "baz" }) }); + + const fixture = TestBed.createComponent(TestComponent); + const component = fixture.componentRef.instance; + expect(component.formGroup.value.name).toBe("baz"); + expect(component.formGroup.dirty).toBe(true); + }); + + it("should not modify form when empty", async () => { + await initServiceWithState({}); + + const fixture = TestBed.createComponent(TestComponent); + const component = fixture.componentRef.instance; + expect(component.formGroup.value.name).toBe("initial name"); + expect(component.formGroup.dirty).toBe(false); + }); + + it("should utilize deserializer", async () => { + await initServiceWithState({ "foo-123": JSON.stringify("bar") }); + + const injector = TestBed.inject(Injector); + + const signal = service.signal({ + key: "foo-123", + initialValue: "foo", + injector, + deserializer: (jsonValue) => "test", + }); + + expect(signal()).toBe("test"); + }); + + it("should not utilize deserializer when empty", async () => { + await initServiceWithState({}); + + const injector = TestBed.inject(Injector); + + const signal = service.signal({ + key: "foo-123", + initialValue: "foo", + injector, + deserializer: (jsonValue) => "test", + }); + + expect(signal()).toBe("foo"); + }); + + it("should send signal updates to message sender", async () => { + await initServiceWithState({}); + + const fixture = TestBed.createComponent(TestComponent); + const component = fixture.componentRef.instance; + component.signal.set("Foobar"); + fixture.detectChanges(); + + expect(messageSenderMock.send).toHaveBeenCalledWith(SAVE_VIEW_CACHE_COMMAND, { + key: "test-signal", + value: JSON.stringify("Foobar"), + }); + }); + + it("should send form updates to message sender", async () => { + await initServiceWithState({}); + + const fixture = TestBed.createComponent(TestComponent); + const component = fixture.componentRef.instance; + component.formGroup.controls.name.setValue("Foobar"); + fixture.detectChanges(); + + expect(messageSenderMock.send).toHaveBeenCalledWith(SAVE_VIEW_CACHE_COMMAND, { + key: "test-form-cache", + value: JSON.stringify({ name: "Foobar" }), + }); + }); + + it("should clear on 2nd navigation", async () => { + await initServiceWithState({}); + + await router.navigate(["a"]); + expect(messageSenderMock.send).toHaveBeenCalledTimes(0); + + await router.navigate(["b"]); + expect(messageSenderMock.send).toHaveBeenCalledWith(ClEAR_VIEW_CACHE_COMMAND, {}); + }); + + it("should ignore cached values when feature flag is off", async () => { + jest.spyOn(configServiceMock, "getFeatureFlag").mockResolvedValue(false); + + await initServiceWithState({ "foo-123": JSON.stringify("bar") }); + + const injector = TestBed.inject(Injector); + + const signal = service.signal({ + key: "foo-123", + initialValue: "foo", + injector, + }); + + // The cached state is ignored + expect(signal()).toBe("foo"); + }); +}); 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 2bdc2fd0d44..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 @@ -10,8 +10,17 @@ import { fromChromeEvent } from "../../browser/from-chrome-event"; export const serializationIndicator = "__json__"; -type serializedObject = { [serializationIndicator]: true; value: string }; +export type SerializedValue = { [serializationIndicator]: true; value: string }; +/** + * Serializes the given object and decorates it to indicate it is serialized. + * + * We have the problem that it is difficult to tell when a value has been serialized, by always + * storing objects decorated with this method, we can easily tell when a value has been serialized and + * deserialize it appropriately. + * @param obj object to decorate and serialize + * @returns a serialized version of the object, decorated to indicate that it is serialized + */ export const objToStore = (obj: any) => { if (obj == null) { return null; @@ -22,7 +31,7 @@ export const objToStore = (obj: any) => { } return { - [serializationIndicator]: true, + [serializationIndicator]: true as const, value: JSON.stringify(obj), }; }; @@ -65,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; @@ -89,23 +102,31 @@ 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(); }); }); } /** Backwards compatible resolution of retrieved object with new serialized storage */ - protected processGetObject(obj: T | serializedObject): T | null { + protected processGetObject(obj: T | SerializedValue): T | null { if (this.isSerialized(obj)) { obj = JSON.parse(obj.value); } @@ -113,8 +134,8 @@ export default abstract class AbstractChromeStorageService } /** Type guard for whether an object is tagged as serialized */ - protected isSerialized(value: T | serializedObject): value is serializedObject { - const asSerialized = value as serializedObject; + protected isSerialized(value: T | SerializedValue): value is SerializedValue { + const asSerialized = value as SerializedValue; return ( asSerialized != null && asSerialized[serializationIndicator] && diff --git a/apps/browser/src/platform/services/abstractions/browser-state.service.ts b/apps/browser/src/platform/services/abstractions/browser-state.service.ts deleted file mode 100644 index c8e2c502e71..00000000000 --- a/apps/browser/src/platform/services/abstractions/browser-state.service.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; - -import { Account } from "../../../models/account"; - -export abstract class BrowserStateService extends BaseStateServiceAbstraction {} diff --git a/apps/browser/src/platform/services/abstractions/browser-task-scheduler.service.ts b/apps/browser/src/platform/services/abstractions/browser-task-scheduler.service.ts new file mode 100644 index 00000000000..58c4eb48897 --- /dev/null +++ b/apps/browser/src/platform/services/abstractions/browser-task-scheduler.service.ts @@ -0,0 +1,33 @@ +import { Observable } from "rxjs"; + +import { TaskSchedulerService, ScheduledTaskName } from "@bitwarden/common/platform/scheduling"; + +export const BrowserTaskSchedulerPortName = "browser-task-scheduler-port"; + +export const BrowserTaskSchedulerPortActions = { + setTimeout: "setTimeout", + setInterval: "setInterval", + clearAlarm: "clearAlarm", +} as const; +export type BrowserTaskSchedulerPortAction = keyof typeof BrowserTaskSchedulerPortActions; + +export type BrowserTaskSchedulerPortMessage = { + action: BrowserTaskSchedulerPortAction; + taskName: ScheduledTaskName; + alarmName?: string; + delayInMs?: number; + intervalInMs?: number; +}; + +export type ActiveAlarm = { + alarmName: string; + startTime: number; + createInfo: chrome.alarms.AlarmCreateInfo; +}; + +export abstract class BrowserTaskSchedulerService extends TaskSchedulerService { + activeAlarms$: Observable; + abstract clearAllScheduledTasks(): Promise; + abstract verifyAlarmsState(): Promise; + abstract clearScheduledAlarm(alarmName: string): Promise; +} 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/background-browser-biometrics.service.ts b/apps/browser/src/platform/services/background-browser-biometrics.service.ts new file mode 100644 index 00000000000..0cd48c45938 --- /dev/null +++ b/apps/browser/src/platform/services/background-browser-biometrics.service.ts @@ -0,0 +1,36 @@ +import { Injectable } from "@angular/core"; + +import { NativeMessagingBackground } from "../../background/nativeMessaging.background"; + +import { BrowserBiometricsService } from "./browser-biometrics.service"; + +@Injectable() +export class BackgroundBrowserBiometricsService extends BrowserBiometricsService { + constructor(private nativeMessagingBackground: () => NativeMessagingBackground) { + super(); + } + + async authenticateBiometric(): Promise { + const responsePromise = this.nativeMessagingBackground().getResponse(); + await this.nativeMessagingBackground().send({ command: "biometricUnlock" }); + const response = await responsePromise; + return response.response === "unlocked"; + } + + async isBiometricUnlockAvailable(): Promise { + const responsePromise = this.nativeMessagingBackground().getResponse(); + await this.nativeMessagingBackground().send({ command: "biometricUnlockAvailable" }); + const response = await responsePromise; + return response.response === "available"; + } + + async biometricsNeedsSetup(): Promise { + return false; + } + + async biometricsSupportsAutoSetup(): Promise { + return false; + } + + async biometricsSetup(): Promise {} +} diff --git a/apps/browser/src/platform/services/browser-biometrics.service.ts b/apps/browser/src/platform/services/browser-biometrics.service.ts new file mode 100644 index 00000000000..84734fb4927 --- /dev/null +++ b/apps/browser/src/platform/services/browser-biometrics.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from "@angular/core"; + +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; + +import { BrowserApi } from "../browser/browser-api"; + +@Injectable() +export abstract class BrowserBiometricsService extends BiometricsService { + async supportsBiometric() { + const platformInfo = await BrowserApi.getPlatformInfo(); + if (platformInfo.os === "mac" || platformInfo.os === "win" || platformInfo.os === "linux") { + return true; + } + return false; + } + + abstract authenticateBiometric(): Promise; + abstract isBiometricUnlockAvailable(): Promise; +} diff --git a/apps/browser/src/platform/services/browser-crypto.service.ts b/apps/browser/src/platform/services/browser-crypto.service.ts index 1242d520213..1d61fb4c8ed 100644 --- a/apps/browser/src/platform/services/browser-crypto.service.ts +++ b/apps/browser/src/platform/services/browser-crypto.service.ts @@ -11,6 +11,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { USER_KEY } from "@bitwarden/common/platform/services/key-state/user-key.state"; @@ -31,6 +32,7 @@ export class BrowserCryptoService extends CryptoService { accountService: AccountService, stateProvider: StateProvider, private biometricStateService: BiometricStateService, + private biometricsService: BiometricsService, kdfConfigService: KdfConfigService, ) { super( @@ -68,7 +70,7 @@ export class BrowserCryptoService extends CryptoService { userId?: UserId, ): Promise { if (keySuffix === KeySuffixOptions.Biometric) { - const biometricsResult = await this.platformUtilService.authenticateBiometric(); + const biometricsResult = await this.biometricsService.authenticateBiometric(); if (!biometricsResult) { return null; 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 37ea37dbf6f..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 @@ -1,89 +1,192 @@ import { objToStore } from "./abstractions/abstract-chrome-storage-api.service"; -import BrowserLocalStorageService from "./browser-local-storage.service"; +import BrowserLocalStorageService, { + RESEED_IN_PROGRESS_KEY, +} from "./browser-local-storage.service"; + +const apiGetLike = + (store: Record) => (key: string, callback: (items: { [key: string]: any }) => void) => { + if (key == null) { + callback(store); + } else { + callback({ [key]: store[key] }); + } + }; describe("BrowserLocalStorageService", () => { let service: BrowserLocalStorageService; let store: Record; + let changeListener: (changes: { [key: string]: chrome.storage.StorageChange }) => void; + + let saveMock: jest.Mock; + let getMock: jest.Mock; + let clearMock: jest.Mock; + let removeMock: jest.Mock; beforeEach(() => { store = {}; - service = new BrowserLocalStorageService(); - }); - - describe("clear", () => { - let clearMock: jest.Mock; - - beforeEach(() => { - clearMock = chrome.storage.local.clear as jest.Mock; + // Record change listener + chrome.storage.local.onChanged.addListener = jest.fn((listener) => { + changeListener = listener; }); - it("uses the api to clear", async () => { - await service.clear(); + service = new BrowserLocalStorageService(); + + // setup mocks + getMock = chrome.storage.local.get as jest.Mock; + getMock.mockImplementation(apiGetLike(store)); + saveMock = chrome.storage.local.set as jest.Mock; + saveMock.mockImplementation((update, callback) => { + Object.entries(update).forEach(([key, value]) => { + store[key] = value; + }); + callback(); + }); + clearMock = chrome.storage.local.clear as jest.Mock; + clearMock.mockImplementation((callback) => { + store = {}; + callback?.(); + }); + removeMock = chrome.storage.local.remove as jest.Mock; + removeMock.mockImplementation((keys, callback) => { + if (Array.isArray(keys)) { + keys.forEach((key) => { + delete store[key]; + }); + } else { + delete store[keys]; + } + + callback(); + }); + }); + + afterEach(() => { + chrome.runtime.lastError = undefined; + jest.resetAllMocks(); + }); + + describe("reseed", () => { + it.each([ + { + key1: objToStore("value1"), + key2: objToStore("value2"), + key3: null, + }, + {}, + ])("saves all data in storage %s", async (testStore) => { + for (const key of Object.keys(testStore) as Array) { + store[key] = testStore[key]; + } + await service.reseed(); + + expect(saveMock).toHaveBeenLastCalledWith( + { ...testStore, [RESEED_IN_PROGRESS_KEY]: objToStore(true) }, + expect.any(Function), + ); + }); + + it.each([ + { + key1: objToStore("value1"), + key2: objToStore("value2"), + key3: null, + }, + {}, + ])("results in the same store %s", async (testStore) => { + for (const key of Object.keys(testStore) as Array) { + store[key] = testStore[key]; + } + await service.reseed(); + + expect(store).toEqual(testStore); + }); + + it("converts non-serialized values to serialized", async () => { + store.key1 = "value1"; + store.key2 = "value2"; + + const expectedStore = { + key1: objToStore("value1"), + key2: objToStore("value2"), + reseedInProgress: objToStore(true), + }; + + await service.reseed(); + + expect(saveMock).toHaveBeenLastCalledWith(expectedStore, expect.any(Function)); + }); + + it("clears data", async () => { + await service.reseed(); 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("getAll", () => { - let getMock: jest.Mock; + describe.each(["get", "has", "save", "remove"] as const)("%s", (method) => { + let interval: string | number | NodeJS.Timeout; - beforeEach(() => { - // setup get - getMock = chrome.storage.local.get as jest.Mock; - getMock.mockImplementation((key, callback) => { - if (key == null) { - callback(store); - } else { - callback({ [key]: store[key] }); - } - }); + afterEach(() => { + if (interval) { + clearInterval(interval); + } }); - it("returns all values", async () => { - store["key1"] = "string"; - store["key2"] = 0; - const result = await service.getAll(); + function startReseed() { + store[RESEED_IN_PROGRESS_KEY] = objToStore(true); + } - expect(result).toEqual(store); + function endReseed() { + delete store[RESEED_IN_PROGRESS_KEY]; + changeListener({ reseedInProgress: { oldValue: true } }); + } + + it("waits for reseed prior to operation", async () => { + startReseed(); + + const promise = service[method]("key", "value"); // note "value" is only used in save, but ignored in other methods + + await expect(promise).not.toBeFulfilled(10); + + endReseed(); + + await expect(promise).toBeResolved(); }); - it("handles empty stores", async () => { - const result = await service.getAll(); - - expect(result).toEqual({}); + it("does not wait if reseed is not in progress", async () => { + const promise = service[method]("key", "value"); + await expect(promise).toBeResolved(1); }); - it("handles stores with null values", async () => { - store["key"] = null; + it("awaits prior reseed operations before starting a new one", async () => { + startReseed(); - const result = await service.getAll(); - expect(result).toEqual(store); - }); + const promise = service.reseed(); - it("handles values processed for storage", async () => { - const obj = { test: 2 }; - const key = "key"; - store[key] = objToStore(obj); + await expect(promise).not.toBeFulfilled(10); - const result = await service.getAll(); + endReseed(); - expect(result).toEqual({ - [key]: obj, - }); - }); - - // This is a test of backwards compatibility before local storage was serialized. - it("handles values that were stored without processing for storage", async () => { - const obj = { test: 2 }; - const key = "key"; - store[key] = obj; - - const result = await service.getAll(); - - expect(result).toEqual({ - [key]: obj, - }); + await expect(promise).toBeResolved(); }); }); }); 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 e1f9f63676f..15cf26d1fbd 100644 --- a/apps/browser/src/platform/services/browser-local-storage.service.ts +++ b/apps/browser/src/platform/services/browser-local-storage.service.ts @@ -1,15 +1,134 @@ -import AbstractChromeStorageService from "./abstractions/abstract-chrome-storage-api.service"; +import { defer, filter, firstValueFrom, map, merge, throwError, timeout } from "rxjs"; + +import AbstractChromeStorageService, { + SerializedValue, + objToStore, +} from "./abstractions/abstract-chrome-storage-api.service"; + +export const RESEED_IN_PROGRESS_KEY = "reseedInProgress"; export default class BrowserLocalStorageService extends AbstractChromeStorageService { constructor() { super(chrome.storage.local); + this.chromeStorageApi.remove(RESEED_IN_PROGRESS_KEY, () => { + return; + }); + } + + /** + * Reads, clears, and re-saves all data in local storage. This is a hack to remove previously stored sensitive data from + * local storage logs. + * + * @see https://github.com/bitwarden/clients/issues/485 + */ + async reseed(): Promise { + try { + await this.save(RESEED_IN_PROGRESS_KEY, true); + const data = await this.getAll(); + await this.clear(); + await this.saveAll(data); + } finally { + await super.remove(RESEED_IN_PROGRESS_KEY); + } + } + + async fillBuffer() { + // Write 4MB of data in chrome.storage.local, log files will hold 4MB of data (by default) + // before forcing a compaction. To force a compaction and have it remove previously saved data, + // we want to fill it's buffer so that anything newly marked for deletion is gone. + // https://github.com/google/leveldb/blob/main/doc/impl.md#log-files + // It's important that if Google uses a different buffer length that we match that, as far as I can tell + // Google uses the default value in Chromium: + // https://github.com/chromium/chromium/blob/148774efa6b3a047369af6179a4248566b39d68f/components/value_store/lazy_leveldb.cc#L65-L66 + const fakeData = "0".repeat(1024 * 1024); // 1MB of data + await new Promise((resolve, reject) => { + this.chromeStorageApi.set( + { + fake_data_1: fakeData, + fake_data_2: fakeData, + fake_data_3: fakeData, + fake_data_4: fakeData, + }, + () => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + + resolve(); + }, + ); + }); + await new Promise((resolve, reject) => { + this.chromeStorageApi.remove( + ["fake_data_1", "fake_data_2", "fake_data_3", "fake_data_4"], + () => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + + resolve(); + }, + ); + }); + } + + override async get(key: string): Promise { + await this.awaitReseed(); + return super.get(key); + } + + override async has(key: string): Promise { + await this.awaitReseed(); + return super.has(key); + } + + override async save(key: string, obj: any): Promise { + await this.awaitReseed(); + return super.save(key, obj); + } + + override async remove(key: string): Promise { + await this.awaitReseed(); + return super.remove(key); + } + + private async awaitReseed(): Promise { + const notReseeding = async () => { + return !(await super.get(RESEED_IN_PROGRESS_KEY)); + }; + + const finishedReseeding = this.updates$.pipe( + filter(({ key, updateType }) => key === RESEED_IN_PROGRESS_KEY && updateType === "remove"), + map(() => true), + ); + + await firstValueFrom( + merge(defer(notReseeding), finishedReseeding).pipe( + filter((v) => v), + timeout({ + // We eventually need to give up and throw an error + first: 5_000, + with: () => + throwError( + () => new Error("Reseeding local storage did not complete in a timely manner."), + ), + }), + ), + ); } /** * Clears local storage */ - async clear() { - await chrome.storage.local.clear(); + private async clear() { + return new Promise((resolve, reject) => { + this.chromeStorageApi.clear(() => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + resolve(); + }); + }); } /** @@ -18,9 +137,13 @@ export default class BrowserLocalStorageService extends AbstractChromeStorageSer * @remarks This method processes values prior to resolving, do not use `chrome.storage.local` directly * @returns Promise resolving to keyed object of all stored data */ - async getAll(): Promise> { - return new Promise((resolve) => { + private async getAll(): Promise> { + 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); @@ -32,4 +155,23 @@ export default class BrowserLocalStorageService extends AbstractChromeStorageSer }); }); } + + private async saveAll(data: Record): Promise { + return new Promise((resolve, reject) => { + const keyedData = Object.entries(data).reduce( + (agg, [key, value]) => { + agg[key] = objToStore(value); + return agg; + }, + {} as Record, + ); + this.chromeStorageApi.set(keyedData, () => { + if (chrome.runtime.lastError) { + return reject(chrome.runtime.lastError); + } + + resolve(); + }); + }); + } } diff --git a/apps/browser/src/platform/services/browser-state.service.spec.ts b/apps/browser/src/platform/services/browser-state.service.spec.ts deleted file mode 100644 index 506f185b649..00000000000 --- a/apps/browser/src/platform/services/browser-state.service.spec.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { mock, MockProxy } from "jest-mock-extended"; - -import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; -import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; -import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; -import { State } from "@bitwarden/common/platform/models/domain/state"; -import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; -import { mockAccountServiceWith } from "@bitwarden/common/spec"; -import { UserId } from "@bitwarden/common/types/guid"; - -import { Account } from "../../models/account"; - -import { DefaultBrowserStateService } from "./default-browser-state.service"; - -describe("Browser State Service", () => { - let secureStorageService: MockProxy; - let diskStorageService: MockProxy; - let logService: MockProxy; - let stateFactory: MockProxy>; - let environmentService: MockProxy; - let tokenService: MockProxy; - let migrationRunner: MockProxy; - - let state: State; - const userId = "userId" as UserId; - const accountService = mockAccountServiceWith(userId); - - let sut: DefaultBrowserStateService; - - beforeEach(() => { - secureStorageService = mock(); - diskStorageService = mock(); - logService = mock(); - stateFactory = mock(); - environmentService = mock(); - tokenService = mock(); - migrationRunner = mock(); - - state = new State(new GlobalState()); - state.accounts[userId] = new Account({ - profile: { userId: userId }, - }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe("state methods", () => { - let memoryStorageService: MockProxy; - - beforeEach(() => { - memoryStorageService = mock(); - const stateGetter = (key: string) => Promise.resolve(state); - memoryStorageService.get.mockImplementation(stateGetter); - - sut = new DefaultBrowserStateService( - diskStorageService, - secureStorageService, - memoryStorageService, - logService, - stateFactory, - accountService, - environmentService, - tokenService, - migrationRunner, - ); - }); - - it("exists", () => { - expect(sut).toBeDefined(); - }); - }); -}); diff --git a/apps/browser/src/platform/services/default-browser-state.service.ts b/apps/browser/src/platform/services/default-browser-state.service.ts deleted file mode 100644 index 92da28efa25..00000000000 --- a/apps/browser/src/platform/services/default-browser-state.service.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; -import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; -import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; -import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; -import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; -import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service"; - -import { Account } from "../../models/account"; - -import { BrowserStateService } from "./abstractions/browser-state.service"; - -export class DefaultBrowserStateService - extends BaseStateService - implements BrowserStateService -{ - protected accountDeserializer = Account.fromJSON; - - constructor( - storageService: AbstractStorageService, - secureStorageService: AbstractStorageService, - memoryStorageService: AbstractStorageService, - logService: LogService, - stateFactory: StateFactory, - accountService: AccountService, - environmentService: EnvironmentService, - tokenService: TokenService, - migrationRunner: MigrationRunner, - ) { - super( - storageService, - secureStorageService, - memoryStorageService, - logService, - stateFactory, - accountService, - environmentService, - tokenService, - migrationRunner, - ); - } - - async addAccount(account: Account) { - // Apply browser overrides to default account values - account = new Account(account); - await super.addAccount(account); - } - - // Overriding the base class to prevent deleting the cache on save. We register a storage listener - // to delete the cache in the constructor above. - protected override async saveAccountToDisk( - account: Account, - options: StorageOptions, - ): Promise { - const storageLocation = options.useSecureStorage - ? this.secureStorageService - : this.storageService; - - await storageLocation.save(`${options.userId}`, account, options); - } -} diff --git a/apps/browser/src/platform/services/foreground-browser-biometrics.ts b/apps/browser/src/platform/services/foreground-browser-biometrics.ts new file mode 100644 index 00000000000..ee55de20108 --- /dev/null +++ b/apps/browser/src/platform/services/foreground-browser-biometrics.ts @@ -0,0 +1,34 @@ +import { BrowserApi } from "../browser/browser-api"; + +import { BrowserBiometricsService } from "./browser-biometrics.service"; + +export class ForegroundBrowserBiometricsService extends BrowserBiometricsService { + async authenticateBiometric(): Promise { + const response = await BrowserApi.sendMessageWithResponse<{ + result: boolean; + error: string; + }>("biometricUnlock"); + if (!response.result) { + throw response.error; + } + return response.result; + } + + async isBiometricUnlockAvailable(): Promise { + const response = await BrowserApi.sendMessageWithResponse<{ + result: boolean; + error: string; + }>("biometricUnlockAvailable"); + return response.result && response.result === true; + } + + async biometricsNeedsSetup(): Promise { + return false; + } + + async biometricsSupportsAutoSetup(): Promise { + return false; + } + + async biometricsSetup(): Promise {} +} 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/platform-utils/background-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts index ec26d6aa29b..da6a8faf3e8 100644 --- a/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts @@ -8,11 +8,10 @@ export class BackgroundPlatformUtilsService extends BrowserPlatformUtilsService constructor( private messagingService: MessagingService, clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, - biometricCallback: () => Promise, win: Window & typeof globalThis, offscreenDocumentService: OffscreenDocumentService, ) { - super(clipboardWriteCallback, biometricCallback, win, offscreenDocumentService); + super(clipboardWriteCallback, win, offscreenDocumentService); } override showToast( diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts index c86c9158019..762380071b7 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts @@ -16,7 +16,7 @@ class TestBrowserPlatformUtilsService extends BrowserPlatformUtilsService { win: Window & typeof globalThis, offscreenDocumentService: OffscreenDocumentService, ) { - super(clipboardSpy, null, win, offscreenDocumentService); + super(clipboardSpy, win, offscreenDocumentService); } showToast( diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts index 26108e60b7e..b47488bdd7d 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts @@ -1,3 +1,4 @@ +import { ExtensionCommand } from "@bitwarden/common/autofill/constants"; import { ClientType, DeviceType } from "@bitwarden/common/enums"; import { ClipboardOptions, @@ -14,7 +15,6 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic constructor( private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, - private biometricCallback: () => Promise, private globalContext: Window | ServiceWorkerGlobalScope, private offscreenDocumentService: OffscreenDocumentService, ) {} @@ -275,18 +275,6 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic return await BrowserClipboardService.read(windowContext); } - async supportsBiometric() { - const platformInfo = await BrowserApi.getPlatformInfo(); - if (platformInfo.os === "mac" || platformInfo.os === "win") { - return true; - } - return false; - } - - authenticateBiometric() { - return this.biometricCallback(); - } - supportsSecureStorage(): boolean { return false; } @@ -298,7 +286,7 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic autofillCommand = "Cmd+Shift+L"; } else if (this.isFirefox()) { autofillCommand = (await browser.commands.getAll()).find( - (c) => c.name === "autofill_login", + (c) => c.name === ExtensionCommand.AutofillLogin, ).shortcut; // Firefox is returning Ctrl instead of Cmd for the modifier key on macOS if // the command is the default one set on installation. @@ -311,7 +299,9 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic } else { await new Promise((resolve) => chrome.commands.getAll((c) => - resolve((autofillCommand = c.find((c) => c.name === "autofill_login").shortcut)), + resolve( + (autofillCommand = c.find((c) => c.name === ExtensionCommand.AutofillLogin).shortcut), + ), ), ); } diff --git a/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts index f775f049e78..5b4b7288d19 100644 --- a/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts @@ -8,11 +8,10 @@ export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService constructor( private toastService: ToastService, clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, - biometricCallback: () => Promise, win: Window & typeof globalThis, offscreenDocumentService: OffscreenDocumentService, ) { - super(clipboardWriteCallback, biometricCallback, win, offscreenDocumentService); + super(clipboardWriteCallback, win, offscreenDocumentService); } override showToast( 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..c2713f70a16 --- /dev/null +++ b/apps/browser/src/platform/services/popup-view-cache-background.service.ts @@ -0,0 +1,98 @@ +import { switchMap, merge, delay, filter, concatMap, map } from "rxjs"; + +import { CommandDefinition, MessageListener } from "@bitwarden/common/platform/messaging"; +import { + POPUP_VIEW_MEMORY, + KeyDefinition, + GlobalStateProvider, +} from "@bitwarden/common/platform/state"; + +import { BrowserApi } from "../browser/browser-api"; +import { fromChromeEvent } from "../browser/from-chrome-event"; + +const popupClosedPortName = "new_popup"; + +/** We cannot use `UserKeyDefinition` because we must be able to store state when there is no active user. */ +export const POPUP_VIEW_CACHE_KEY = KeyDefinition.record( + POPUP_VIEW_MEMORY, + "popup-view-cache", + { + deserializer: (jsonValue) => jsonValue, + }, +); + +export const POPUP_ROUTE_HISTORY_KEY = new KeyDefinition( + POPUP_VIEW_MEMORY, + "popup-route-history", + { + deserializer: (jsonValue) => jsonValue, + }, +); + +export const SAVE_VIEW_CACHE_COMMAND = new CommandDefinition<{ + key: string; + value: string; +}>("save-view-cache"); + +export const ClEAR_VIEW_CACHE_COMMAND = new CommandDefinition("clear-view-cache"); + +export class PopupViewCacheBackgroundService { + private popupViewCacheState = this.globalStateProvider.get(POPUP_VIEW_CACHE_KEY); + private popupRouteHistoryState = this.globalStateProvider.get(POPUP_ROUTE_HISTORY_KEY); + + constructor( + private messageListener: MessageListener, + private globalStateProvider: GlobalStateProvider, + ) {} + + startObservingTabChanges() { + this.messageListener + .messages$(SAVE_VIEW_CACHE_COMMAND) + .pipe( + concatMap(async ({ key, value }) => + this.popupViewCacheState.update((state) => ({ + ...state, + [key]: value, + })), + ), + ) + .subscribe(); + + merge( + // on tab changed, excluding extension tabs + fromChromeEvent(chrome.tabs.onActivated).pipe( + switchMap(([tabInfo]) => BrowserApi.getTab(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.popupViewCacheState.update(() => ({}), { shouldUpdate: this.objNotEmpty }), + 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/services/task-scheduler/background-task-scheduler.service.spec.ts b/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.spec.ts new file mode 100644 index 00000000000..ded57a5e85d --- /dev/null +++ b/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.spec.ts @@ -0,0 +1,129 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { Observable } from "rxjs"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; +import { GlobalState, StateProvider } from "@bitwarden/common/platform/state"; + +import { createPortSpyMock } from "../../../autofill/spec/autofill-mocks"; +import { + flushPromises, + sendPortMessage, + triggerPortOnDisconnectEvent, + triggerRuntimeOnConnectEvent, +} from "../../../autofill/spec/testing-utils"; +import { + BrowserTaskSchedulerPortActions, + BrowserTaskSchedulerPortName, +} from "../abstractions/browser-task-scheduler.service"; + +import { BackgroundTaskSchedulerService } from "./background-task-scheduler.service"; + +describe("BackgroundTaskSchedulerService", () => { + let logService: MockProxy; + let stateProvider: MockProxy; + let globalStateMock: MockProxy>; + let portMock: chrome.runtime.Port; + let backgroundTaskSchedulerService: BackgroundTaskSchedulerService; + + beforeEach(() => { + logService = mock(); + globalStateMock = mock>({ + state$: mock>(), + update: jest.fn((callback) => callback([], {} as any)), + }); + stateProvider = mock({ + getGlobal: jest.fn(() => globalStateMock), + }); + portMock = createPortSpyMock(BrowserTaskSchedulerPortName); + backgroundTaskSchedulerService = new BackgroundTaskSchedulerService(logService, stateProvider); + jest.spyOn(globalThis, "setTimeout"); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("ports on connect", () => { + it("ignores port connections that do not have the correct task scheduler port name", () => { + const portMockWithDifferentName = createPortSpyMock("different-name"); + triggerRuntimeOnConnectEvent(portMockWithDifferentName); + + expect(portMockWithDifferentName.onMessage.addListener).not.toHaveBeenCalled(); + expect(portMockWithDifferentName.onDisconnect.addListener).not.toHaveBeenCalled(); + }); + + it("sets up onMessage and onDisconnect listeners for connected ports", () => { + triggerRuntimeOnConnectEvent(portMock); + + expect(portMock.onMessage.addListener).toHaveBeenCalled(); + expect(portMock.onDisconnect.addListener).toHaveBeenCalled(); + }); + }); + + describe("ports on disconnect", () => { + it("removes the port from the set of connected ports", () => { + triggerRuntimeOnConnectEvent(portMock); + expect(backgroundTaskSchedulerService["ports"].size).toBe(1); + + triggerPortOnDisconnectEvent(portMock); + expect(backgroundTaskSchedulerService["ports"].size).toBe(0); + expect(portMock.onMessage.removeListener).toHaveBeenCalled(); + expect(portMock.onDisconnect.removeListener).toHaveBeenCalled(); + }); + }); + + describe("port message handlers", () => { + beforeEach(() => { + triggerRuntimeOnConnectEvent(portMock); + backgroundTaskSchedulerService.registerTaskHandler( + ScheduledTaskNames.loginStrategySessionTimeout, + jest.fn(), + ); + }); + + it("sets a setTimeout backup alarm", async () => { + sendPortMessage(portMock, { + action: BrowserTaskSchedulerPortActions.setTimeout, + taskName: ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs: 1000, + }); + await flushPromises(); + + expect(globalThis.setTimeout).toHaveBeenCalled(); + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { delayInMinutes: 0.5 }, + expect.any(Function), + ); + }); + + it("sets a setInterval backup alarm", async () => { + sendPortMessage(portMock, { + action: BrowserTaskSchedulerPortActions.setInterval, + taskName: ScheduledTaskNames.loginStrategySessionTimeout, + intervalInMs: 600000, + }); + await flushPromises(); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { delayInMinutes: 10, periodInMinutes: 10 }, + expect.any(Function), + ); + }); + + it("clears a scheduled alarm", async () => { + sendPortMessage(portMock, { + action: BrowserTaskSchedulerPortActions.clearAlarm, + alarmName: ScheduledTaskNames.loginStrategySessionTimeout, + }); + await flushPromises(); + + expect(chrome.alarms.clear).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + expect.any(Function), + ); + }); + }); +}); diff --git a/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.ts b/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.ts new file mode 100644 index 00000000000..23b580988f8 --- /dev/null +++ b/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.ts @@ -0,0 +1,75 @@ +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { StateProvider } from "@bitwarden/common/platform/state"; + +import { BrowserApi } from "../../browser/browser-api"; +import { + BrowserTaskSchedulerPortActions, + BrowserTaskSchedulerPortMessage, + BrowserTaskSchedulerPortName, +} from "../abstractions/browser-task-scheduler.service"; + +import { BrowserTaskSchedulerServiceImplementation } from "./browser-task-scheduler.service"; + +export class BackgroundTaskSchedulerService extends BrowserTaskSchedulerServiceImplementation { + private ports: Set = new Set(); + + constructor(logService: LogService, stateProvider: StateProvider) { + super(logService, stateProvider); + + BrowserApi.addListener(chrome.runtime.onConnect, this.handlePortOnConnect); + } + + /** + * Handles a port connection made from the foreground task scheduler. + * + * @param port - The port that was connected. + */ + private handlePortOnConnect = (port: chrome.runtime.Port) => { + if (port.name !== BrowserTaskSchedulerPortName) { + return; + } + + this.ports.add(port); + port.onMessage.addListener(this.handlePortMessage); + port.onDisconnect.addListener(this.handlePortOnDisconnect); + }; + + /** + * Handles a port disconnection. + * + * @param port - The port that was disconnected. + */ + private handlePortOnDisconnect = (port: chrome.runtime.Port) => { + port.onMessage.removeListener(this.handlePortMessage); + port.onDisconnect.removeListener(this.handlePortOnDisconnect); + this.ports.delete(port); + }; + + /** + * Handles a message from a port. + * + * @param message - The message that was received. + * @param port - The port that sent the message. + */ + private handlePortMessage = ( + message: BrowserTaskSchedulerPortMessage, + port: chrome.runtime.Port, + ) => { + const isTaskSchedulerPort = port.name === BrowserTaskSchedulerPortName; + const { action, taskName, alarmName, delayInMs, intervalInMs } = message; + + if (isTaskSchedulerPort && action === BrowserTaskSchedulerPortActions.setTimeout) { + super.setTimeout(taskName, delayInMs); + return; + } + + if (isTaskSchedulerPort && action === BrowserTaskSchedulerPortActions.setInterval) { + super.setInterval(taskName, intervalInMs); + return; + } + + if (isTaskSchedulerPort && action === BrowserTaskSchedulerPortActions.clearAlarm) { + super.clearScheduledAlarm(alarmName).catch((error) => this.logService.error(error)); + } + }; +} diff --git a/apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.spec.ts b/apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.spec.ts new file mode 100644 index 00000000000..d72ba942051 --- /dev/null +++ b/apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.spec.ts @@ -0,0 +1,463 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject, Observable } from "rxjs"; + +import { ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; +import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; +import { GlobalState, StateProvider } from "@bitwarden/common/platform/state"; + +import { flushPromises, triggerOnAlarmEvent } from "../../../autofill/spec/testing-utils"; +import { + ActiveAlarm, + BrowserTaskSchedulerService, +} from "../abstractions/browser-task-scheduler.service"; + +import { BrowserTaskSchedulerServiceImplementation } from "./browser-task-scheduler.service"; + +jest.mock("rxjs", () => { + const actualModule = jest.requireActual("rxjs"); + return { + ...actualModule, + firstValueFrom: jest.fn((state$: BehaviorSubject) => state$.value), + }; +}); + +function setupGlobalBrowserMock(overrides: Partial = {}) { + globalThis.browser.alarms = { + create: jest.fn(), + clear: jest.fn(), + get: jest.fn(), + getAll: jest.fn(), + clearAll: jest.fn(), + onAlarm: { + addListener: jest.fn(), + removeListener: jest.fn(), + hasListener: jest.fn(), + }, + ...overrides, + }; +} + +describe("BrowserTaskSchedulerService", () => { + const callback = jest.fn(); + const delayInMinutes = 2; + let activeAlarmsMock$: BehaviorSubject; + let logService: MockProxy; + let stateProvider: MockProxy; + let globalStateMock: MockProxy>; + let browserTaskSchedulerService: BrowserTaskSchedulerService; + let activeAlarms: ActiveAlarm[] = []; + const eventUploadsIntervalCreateInfo = { periodInMinutes: 5, delayInMinutes: 5 }; + const scheduleNextSyncIntervalCreateInfo = { periodInMinutes: 5, delayInMinutes: 5 }; + + beforeEach(() => { + jest.useFakeTimers(); + activeAlarms = [ + mock({ + alarmName: ScheduledTaskNames.eventUploadsInterval, + createInfo: eventUploadsIntervalCreateInfo, + }), + mock({ + alarmName: ScheduledTaskNames.scheduleNextSyncInterval, + createInfo: scheduleNextSyncIntervalCreateInfo, + }), + mock({ + alarmName: ScheduledTaskNames.fido2ClientAbortTimeout, + startTime: Date.now() - 60001, + createInfo: { delayInMinutes: 1, periodInMinutes: undefined }, + }), + ]; + activeAlarmsMock$ = new BehaviorSubject(activeAlarms); + logService = mock(); + globalStateMock = mock>({ + state$: mock>(), + update: jest.fn((callback) => callback([], {} as any)), + }); + stateProvider = mock({ + getGlobal: jest.fn(() => globalStateMock), + }); + browserTaskSchedulerService = new BrowserTaskSchedulerServiceImplementation( + logService, + stateProvider, + ); + browserTaskSchedulerService.activeAlarms$ = activeAlarmsMock$; + browserTaskSchedulerService.registerTaskHandler( + ScheduledTaskNames.loginStrategySessionTimeout, + callback, + ); + // @ts-expect-error mocking global browser object + // eslint-disable-next-line no-global-assign + globalThis.browser = {}; + chrome.alarms.get = jest.fn().mockImplementation((_name, callback) => callback(undefined)); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.clearAllTimers(); + jest.useRealTimers(); + + // eslint-disable-next-line no-global-assign + globalThis.browser = undefined; + }); + + describe("setTimeout", () => { + it("triggers an error when setting a timeout for a task that is not registered", async () => { + expect(() => + browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.notificationsReconnectTimeout, + 1000, + ), + ).toThrow( + `Task handler for ${ScheduledTaskNames.notificationsReconnectTimeout} not registered. Unable to schedule task.`, + ); + }); + + it("creates a timeout alarm", async () => { + browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMinutes * 60 * 1000, + ); + await flushPromises(); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { delayInMinutes }, + expect.any(Function), + ); + }); + + it("skips creating a duplicate timeout alarm", async () => { + const mockAlarm = mock(); + chrome.alarms.get = jest.fn().mockImplementation((_name, callback) => callback(mockAlarm)); + + browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMinutes * 60 * 1000, + ); + + expect(chrome.alarms.create).not.toHaveBeenCalled(); + }); + + describe("when the task is scheduled to be triggered in less than the minimum possible delay", () => { + const delayInMs = 25000; + + it("sets a timeout using the global setTimeout API", async () => { + jest.spyOn(globalThis, "setTimeout"); + + browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs, + ); + await flushPromises(); + + expect(globalThis.setTimeout).toHaveBeenCalledWith(expect.any(Function), delayInMs); + }); + + it("sets a fallback alarm", async () => { + const delayInMs = 15000; + browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs, + ); + await flushPromises(); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { delayInMinutes: 0.5 }, + expect.any(Function), + ); + }); + + it("sets the fallback for a minimum of 1 minute if the environment not for Chrome", async () => { + setupGlobalBrowserMock(); + + browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs, + ); + await flushPromises(); + + expect(browser.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { delayInMinutes: 1 }, + ); + }); + + it("clears the fallback alarm when the setTimeout is triggered", async () => { + jest.useFakeTimers(); + + browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs, + ); + jest.advanceTimersByTime(delayInMs); + await flushPromises(); + + expect(chrome.alarms.clear).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + expect.any(Function), + ); + }); + }); + + it("returns a subscription that can be used to clear the timeout", () => { + jest.spyOn(globalThis, "clearTimeout"); + + const timeoutSubscription = browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + 10000, + ); + + timeoutSubscription.unsubscribe(); + + expect(chrome.alarms.clear).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + expect.any(Function), + ); + expect(globalThis.clearTimeout).toHaveBeenCalled(); + }); + + it("clears alarms in non-chrome environments", () => { + setupGlobalBrowserMock(); + + const timeoutSubscription = browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + 10000, + ); + timeoutSubscription.unsubscribe(); + + expect(browser.alarms.clear).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + ); + }); + }); + + describe("setInterval", () => { + it("triggers an error when setting an interval for a task that is not registered", async () => { + expect(() => { + browserTaskSchedulerService.setInterval( + ScheduledTaskNames.notificationsReconnectTimeout, + 1000, + ); + }).toThrow( + `Task handler for ${ScheduledTaskNames.notificationsReconnectTimeout} not registered. Unable to schedule task.`, + ); + }); + + describe("setting an interval that is less than 1 minute", () => { + const intervalInMs = 10000; + + it("sets up stepped alarms that trigger behavior after the first minute of setInterval execution", async () => { + browserTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + intervalInMs, + ); + await flushPromises(); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__0`, + { periodInMinutes: 0.6666666666666666, delayInMinutes: 0.5 }, + expect.any(Function), + ); + expect(chrome.alarms.create).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__1`, + { periodInMinutes: 0.6666666666666666, delayInMinutes: 0.6666666666666666 }, + expect.any(Function), + ); + expect(chrome.alarms.create).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__2`, + { periodInMinutes: 0.6666666666666666, delayInMinutes: 0.8333333333333333 }, + expect.any(Function), + ); + expect(chrome.alarms.create).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__3`, + { periodInMinutes: 0.6666666666666666, delayInMinutes: 1 }, + expect.any(Function), + ); + }); + + it("sets an interval using the global setInterval API", async () => { + jest.spyOn(globalThis, "setInterval"); + + browserTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + intervalInMs, + ); + await flushPromises(); + + expect(globalThis.setInterval).toHaveBeenCalledWith(expect.any(Function), intervalInMs); + }); + + it("clears the global setInterval instance once the interval has elapsed the minimum required delay for an alarm", async () => { + jest.useFakeTimers(); + jest.spyOn(globalThis, "clearInterval"); + + browserTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + intervalInMs, + ); + await flushPromises(); + jest.advanceTimersByTime(50000); + + expect(globalThis.clearInterval).toHaveBeenCalledWith(expect.any(Number)); + }); + }); + + it("creates an interval alarm", async () => { + const periodInMinutes = 2; + const initialDelayInMs = 1000; + + browserTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + periodInMinutes * 60 * 1000, + initialDelayInMs, + ); + await flushPromises(); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { periodInMinutes, delayInMinutes: 0.5 }, + expect.any(Function), + ); + }); + + it("defaults the alarm's delay in minutes to the interval in minutes if the delay is not specified", async () => { + const periodInMinutes = 2; + browserTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + periodInMinutes * 60 * 1000, + ); + await flushPromises(); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { periodInMinutes, delayInMinutes: periodInMinutes }, + expect.any(Function), + ); + }); + + it("returns a subscription that can be used to clear an interval alarm", () => { + jest.spyOn(globalThis, "clearInterval"); + + const intervalSubscription = browserTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + 600000, + ); + + intervalSubscription.unsubscribe(); + + expect(chrome.alarms.clear).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + expect.any(Function), + ); + expect(globalThis.clearInterval).not.toHaveBeenCalled(); + }); + + it("returns a subscription that can be used to clear all stepped interval alarms", () => { + jest.spyOn(globalThis, "clearInterval"); + + const intervalSubscription = browserTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + 10000, + ); + + intervalSubscription.unsubscribe(); + + expect(chrome.alarms.clear).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__0`, + expect.any(Function), + ); + expect(chrome.alarms.clear).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__1`, + expect.any(Function), + ); + expect(chrome.alarms.clear).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__2`, + expect.any(Function), + ); + expect(chrome.alarms.clear).toHaveBeenCalledWith( + `${ScheduledTaskNames.loginStrategySessionTimeout}__3`, + expect.any(Function), + ); + expect(globalThis.clearInterval).toHaveBeenCalled(); + }); + }); + + describe("verifyAlarmsState", () => { + it("skips recovering a scheduled task if an existing alarm for the task is present", async () => { + chrome.alarms.get = jest + .fn() + .mockImplementation((_name, callback) => callback(mock())); + + await browserTaskSchedulerService.verifyAlarmsState(); + + expect(chrome.alarms.create).not.toHaveBeenCalled(); + expect(callback).not.toHaveBeenCalled(); + }); + + describe("extension alarm is not set", () => { + it("triggers the task when the task should have triggered", async () => { + const fido2Callback = jest.fn(); + browserTaskSchedulerService.registerTaskHandler( + ScheduledTaskNames.fido2ClientAbortTimeout, + fido2Callback, + ); + + await browserTaskSchedulerService.verifyAlarmsState(); + + expect(fido2Callback).toHaveBeenCalled(); + }); + + it("schedules an alarm for the task when it has not yet triggered ", async () => { + const syncCallback = jest.fn(); + browserTaskSchedulerService.registerTaskHandler( + ScheduledTaskNames.scheduleNextSyncInterval, + syncCallback, + ); + + await browserTaskSchedulerService.verifyAlarmsState(); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.scheduleNextSyncInterval, + scheduleNextSyncIntervalCreateInfo, + expect.any(Function), + ); + }); + }); + }); + + describe("triggering a task", () => { + it("triggers a task when an onAlarm event is triggered", () => { + const alarm = mock({ + name: ScheduledTaskNames.loginStrategySessionTimeout, + }); + + triggerOnAlarmEvent(alarm); + + expect(callback).toHaveBeenCalled(); + }); + }); + + describe("clearAllScheduledTasks", () => { + it("clears all scheduled tasks and extension alarms", async () => { + // @ts-expect-error mocking global state update method + globalStateMock.update = jest.fn((callback) => { + const stateValue = callback([], {} as any); + activeAlarmsMock$.next(stateValue); + return stateValue; + }); + + await browserTaskSchedulerService.clearAllScheduledTasks(); + + expect(chrome.alarms.clearAll).toHaveBeenCalled(); + expect(activeAlarmsMock$.value).toEqual([]); + }); + + it("clears all extension alarms within a non Chrome environment", async () => { + setupGlobalBrowserMock(); + + await browserTaskSchedulerService.clearAllScheduledTasks(); + + expect(browser.alarms.clearAll).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.ts b/apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.ts new file mode 100644 index 00000000000..187742f5891 --- /dev/null +++ b/apps/browser/src/platform/services/task-scheduler/browser-task-scheduler.service.ts @@ -0,0 +1,427 @@ +import { firstValueFrom, map, Observable, Subscription } from "rxjs"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { + DefaultTaskSchedulerService, + ScheduledTaskName, +} from "@bitwarden/common/platform/scheduling"; +import { + TASK_SCHEDULER_DISK, + GlobalState, + KeyDefinition, + StateProvider, +} from "@bitwarden/common/platform/state"; + +import { BrowserApi } from "../../browser/browser-api"; +import { + ActiveAlarm, + BrowserTaskSchedulerService, +} from "../abstractions/browser-task-scheduler.service"; + +const ACTIVE_ALARMS = new KeyDefinition(TASK_SCHEDULER_DISK, "activeAlarms", { + deserializer: (value: ActiveAlarm[]) => value ?? [], +}); + +export class BrowserTaskSchedulerServiceImplementation + extends DefaultTaskSchedulerService + implements BrowserTaskSchedulerService +{ + private activeAlarmsState: GlobalState; + readonly activeAlarms$: Observable; + + constructor( + logService: LogService, + private stateProvider: StateProvider, + ) { + super(logService); + + this.activeAlarmsState = this.stateProvider.getGlobal(ACTIVE_ALARMS); + this.activeAlarms$ = this.activeAlarmsState.state$.pipe( + map((activeAlarms) => activeAlarms ?? []), + ); + + this.setupOnAlarmListener(); + } + + /** + * Sets a timeout to execute a callback after a delay. If the delay is less + * than 1 minute, it will use the global setTimeout. Otherwise, it will + * create a browser extension alarm to handle the delay. + * + * @param taskName - The name of the task, used in defining the alarm. + * @param delayInMs - The delay in milliseconds. + */ + setTimeout(taskName: ScheduledTaskName, delayInMs: number): Subscription { + let timeoutHandle: number | NodeJS.Timeout; + this.validateRegisteredTask(taskName); + + const delayInMinutes = delayInMs / 1000 / 60; + this.scheduleAlarm(taskName, { + delayInMinutes: this.getUpperBoundDelayInMinutes(delayInMinutes), + }).catch((error) => this.logService.error("Failed to schedule alarm", error)); + + // If the delay is less than a minute, we want to attempt to trigger the task through a setTimeout. + // The alarm previously scheduled will be used as a backup in case the setTimeout fails. + if (delayInMinutes < this.getUpperBoundDelayInMinutes(delayInMinutes)) { + timeoutHandle = globalThis.setTimeout(async () => { + await this.clearScheduledAlarm(taskName); + await this.triggerTask(taskName); + }, delayInMs); + } + + return new Subscription(() => { + if (timeoutHandle) { + globalThis.clearTimeout(timeoutHandle); + } + this.clearScheduledAlarm(taskName).catch((error) => + this.logService.error("Failed to clear alarm", error), + ); + }); + } + + /** + * Sets an interval to execute a callback at each interval. If the interval is + * less than 1 minute, it will use the global setInterval. Otherwise, it will + * create a browser extension alarm to handle the interval. + * + * @param taskName - The name of the task, used in defining the alarm. + * @param intervalInMs - The interval in milliseconds. + * @param initialDelayInMs - The initial delay in milliseconds. + */ + setInterval( + taskName: ScheduledTaskName, + intervalInMs: number, + initialDelayInMs?: number, + ): Subscription { + this.validateRegisteredTask(taskName); + + const intervalInMinutes = intervalInMs / 1000 / 60; + const initialDelayInMinutes = initialDelayInMs + ? initialDelayInMs / 1000 / 60 + : intervalInMinutes; + + if (intervalInMinutes < this.getUpperBoundDelayInMinutes(intervalInMinutes)) { + return this.setupSteppedIntervalAlarms(taskName, intervalInMs); + } + + this.scheduleAlarm(taskName, { + periodInMinutes: this.getUpperBoundDelayInMinutes(intervalInMinutes), + delayInMinutes: this.getUpperBoundDelayInMinutes(initialDelayInMinutes), + }).catch((error) => this.logService.error("Failed to schedule alarm", error)); + + return new Subscription(() => + this.clearScheduledAlarm(taskName).catch((error) => + this.logService.error("Failed to clear alarm", error), + ), + ); + } + + /** + * Used in cases where the interval is less than 1 minute. This method will set up a setInterval + * to initialize expected recurring behavior, then create a series of alarms to handle the + * expected scheduled task through the alarms api. This is necessary because the alarms + * api does not support intervals less than 1 minute. + * + * @param taskName - The name of the task + * @param intervalInMs - The interval in milliseconds. + */ + private setupSteppedIntervalAlarms( + taskName: ScheduledTaskName, + intervalInMs: number, + ): Subscription { + const alarmMinDelayInMinutes = this.getAlarmMinDelayInMinutes(); + const intervalInMinutes = intervalInMs / 1000 / 60; + const numberOfAlarmsToCreate = Math.ceil(Math.ceil(1 / intervalInMinutes) / 2) + 1; + const steppedAlarmPeriodInMinutes = alarmMinDelayInMinutes + intervalInMinutes; + const steppedAlarmNames: string[] = []; + for (let alarmIndex = 0; alarmIndex < numberOfAlarmsToCreate; alarmIndex++) { + const steppedAlarmName = `${taskName}__${alarmIndex}`; + steppedAlarmNames.push(steppedAlarmName); + + const delayInMinutes = this.getUpperBoundDelayInMinutes( + alarmMinDelayInMinutes + intervalInMinutes * alarmIndex, + ); + + this.clearScheduledAlarm(steppedAlarmName) + .then(() => + this.scheduleAlarm(steppedAlarmName, { + periodInMinutes: steppedAlarmPeriodInMinutes, + delayInMinutes, + }).catch((error) => this.logService.error("Failed to schedule alarm", error)), + ) + .catch((error) => this.logService.error("Failed to clear alarm", error)); + } + + let elapsedMs = 0; + const intervalHandle: number | NodeJS.Timeout = globalThis.setInterval(async () => { + elapsedMs += intervalInMs; + const elapsedMinutes = elapsedMs / 1000 / 60; + + if (elapsedMinutes >= alarmMinDelayInMinutes) { + globalThis.clearInterval(intervalHandle); + return; + } + + await this.triggerTask(taskName, intervalInMinutes); + }, intervalInMs); + + return new Subscription(() => { + if (intervalHandle) { + globalThis.clearInterval(intervalHandle); + } + steppedAlarmNames.forEach((alarmName) => + this.clearScheduledAlarm(alarmName).catch((error) => + this.logService.error("Failed to clear alarm", error), + ), + ); + }); + } + + /** + * Clears all scheduled tasks by clearing all browser extension + * alarms and resetting the active alarms state. + */ + async clearAllScheduledTasks(): Promise { + await this.clearAllAlarms(); + await this.updateActiveAlarms([]); + } + + /** + * Verifies the state of the active alarms by checking if + * any alarms have been missed or need to be created. + */ + async verifyAlarmsState(): Promise { + const currentTime = Date.now(); + const activeAlarms = await this.getActiveAlarms(); + + for (const alarm of activeAlarms) { + const { alarmName, startTime, createInfo } = alarm; + const existingAlarm = await this.getAlarm(alarmName); + if (existingAlarm) { + continue; + } + + const shouldAlarmHaveBeenTriggered = createInfo.when && createInfo.when < currentTime; + const hasSetTimeoutAlarmExceededDelay = + !createInfo.periodInMinutes && + createInfo.delayInMinutes && + startTime + createInfo.delayInMinutes * 60 * 1000 < currentTime; + if (shouldAlarmHaveBeenTriggered || hasSetTimeoutAlarmExceededDelay) { + await this.triggerTask(alarmName); + continue; + } + + this.scheduleAlarm(alarmName, createInfo).catch((error) => + this.logService.error("Failed to schedule alarm", error), + ); + } + } + + /** + * Creates a browser extension alarm with the given name and create info. + * + * @param alarmName - The name of the alarm. + * @param createInfo - The alarm create info. + */ + private async scheduleAlarm( + alarmName: string, + createInfo: chrome.alarms.AlarmCreateInfo, + ): Promise { + const existingAlarm = await this.getAlarm(alarmName); + if (existingAlarm) { + this.logService.debug(`Alarm ${alarmName} already exists. Skipping creation.`); + return; + } + + await this.createAlarm(alarmName, createInfo); + await this.setActiveAlarm(alarmName, createInfo); + } + + /** + * Gets the active alarms from state. + */ + private async getActiveAlarms(): Promise { + return await firstValueFrom(this.activeAlarms$); + } + + /** + * Sets an active alarm in state. + * + * @param alarmName - The name of the active alarm to set. + * @param createInfo - The creation info of the active alarm. + */ + private async setActiveAlarm( + alarmName: string, + createInfo: chrome.alarms.AlarmCreateInfo, + ): Promise { + const activeAlarms = await this.getActiveAlarms(); + const filteredAlarms = activeAlarms.filter((alarm) => alarm.alarmName !== alarmName); + filteredAlarms.push({ + alarmName, + startTime: Date.now(), + createInfo, + }); + await this.updateActiveAlarms(filteredAlarms); + } + + /** + * Deletes an active alarm from state. + * + * @param alarmName - The name of the active alarm to delete. + */ + private async deleteActiveAlarm(alarmName: string): Promise { + const activeAlarms = await this.getActiveAlarms(); + const filteredAlarms = activeAlarms.filter((alarm) => alarm.alarmName !== alarmName); + await this.updateActiveAlarms(filteredAlarms || []); + } + + /** + * Clears a scheduled alarm by its name and deletes it from the active alarms state. + * + * @param alarmName - The name of the alarm to clear. + */ + async clearScheduledAlarm(alarmName: string): Promise { + const wasCleared = await this.clearAlarm(alarmName); + if (wasCleared) { + await this.deleteActiveAlarm(alarmName); + } + } + + /** + * Updates the active alarms state with the given alarms. + * + * @param alarms - The alarms to update the state with. + */ + private async updateActiveAlarms(alarms: ActiveAlarm[]): Promise { + await this.activeAlarmsState.update(() => alarms); + } + + /** + * Sets up the on alarm listener to handle alarms. + */ + private setupOnAlarmListener(): void { + BrowserApi.addListener(chrome.alarms.onAlarm, this.handleOnAlarm); + } + + /** + * Handles on alarm events, triggering the alarm if a handler exists. + * + * @param alarm - The alarm to handle. + */ + private handleOnAlarm = async (alarm: chrome.alarms.Alarm): Promise => { + const { name, periodInMinutes } = alarm; + await this.triggerTask(name, periodInMinutes); + }; + + /** + * Triggers an alarm by calling its handler and + * deleting it if it is a one-time alarm. + * + * @param alarmName - The name of the alarm to trigger. + * @param periodInMinutes - The period in minutes of an interval alarm. + */ + protected async triggerTask(alarmName: string, periodInMinutes?: number): Promise { + const taskName = this.getTaskFromAlarmName(alarmName); + const handler = this.taskHandlers.get(taskName); + if (!periodInMinutes) { + await this.deleteActiveAlarm(alarmName); + } + + if (handler) { + handler(); + } + } + + /** + * Parses and returns the task name from an alarm name. + * + * @param alarmName - The alarm name to parse. + */ + protected getTaskFromAlarmName(alarmName: string): ScheduledTaskName { + return alarmName.split("__")[0] as ScheduledTaskName; + } + + /** + * Clears a new alarm with the given name and create info. Returns a promise + * that indicates when the alarm has been cleared successfully. + * + * @param alarmName - The name of the alarm to create. + */ + private async clearAlarm(alarmName: string): Promise { + if (this.isNonChromeEnvironment()) { + return browser.alarms.clear(alarmName); + } + + return new Promise((resolve) => chrome.alarms.clear(alarmName, resolve)); + } + + /** + * Clears all alarms that have been set by the extension. Returns a promise + * that indicates when all alarms have been cleared successfully. + */ + private clearAllAlarms(): Promise { + if (this.isNonChromeEnvironment()) { + return browser.alarms.clearAll(); + } + + return new Promise((resolve) => chrome.alarms.clearAll(resolve)); + } + + /** + * Creates a new alarm with the given name and create info. + * + * @param alarmName - The name of the alarm to create. + * @param createInfo - The creation info for the alarm. + */ + private async createAlarm( + alarmName: string, + createInfo: chrome.alarms.AlarmCreateInfo, + ): Promise { + if (this.isNonChromeEnvironment()) { + return browser.alarms.create(alarmName, createInfo); + } + + return new Promise((resolve) => chrome.alarms.create(alarmName, createInfo, resolve)); + } + + /** + * Gets the alarm with the given name. + * + * @param alarmName - The name of the alarm to get. + */ + private getAlarm(alarmName: string): Promise { + if (this.isNonChromeEnvironment()) { + return browser.alarms.get(alarmName); + } + + return new Promise((resolve) => chrome.alarms.get(alarmName, resolve)); + } + + /** + * Checks if the environment is a non-Chrome environment. This is used to determine + * if the browser alarms API should be used in place of the chrome alarms API. This + * is necessary because the `chrome` polyfill that Mozilla implements does not allow + * passing the callback parameter in the same way most `chrome.alarm` api calls allow. + */ + private isNonChromeEnvironment(): boolean { + return typeof browser !== "undefined" && !!browser.alarms; + } + + /** + * Gets the minimum delay in minutes for an alarm. This is used to ensure that the + * delay is at least 1 minute in non-Chrome environments. In Chrome environments, the + * delay can be as low as 0.5 minutes. + */ + private getAlarmMinDelayInMinutes(): number { + return this.isNonChromeEnvironment() ? 1 : 0.5; + } + + /** + * Gets the upper bound delay in minutes for a given delay in minutes. + * + * @param delayInMinutes - The delay in minutes. + */ + private getUpperBoundDelayInMinutes(delayInMinutes: number): number { + return Math.max(this.getAlarmMinDelayInMinutes(), delayInMinutes); + } +} diff --git a/apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.spec.ts b/apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.spec.ts new file mode 100644 index 00000000000..e0ee49c5fa1 --- /dev/null +++ b/apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.spec.ts @@ -0,0 +1,79 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { Observable } from "rxjs"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; +import { GlobalState, StateProvider } from "@bitwarden/common/platform/state"; + +import { createPortSpyMock } from "../../../autofill/spec/autofill-mocks"; +import { flushPromises } from "../../../autofill/spec/testing-utils"; +import { + BrowserTaskSchedulerPortActions, + BrowserTaskSchedulerPortName, +} from "../abstractions/browser-task-scheduler.service"; + +import { ForegroundTaskSchedulerService } from "./foreground-task-scheduler.service"; + +describe("ForegroundTaskSchedulerService", () => { + let logService: MockProxy; + let stateProvider: MockProxy; + let globalStateMock: MockProxy>; + let portMock: chrome.runtime.Port; + let foregroundTaskSchedulerService: ForegroundTaskSchedulerService; + + beforeEach(() => { + logService = mock(); + globalStateMock = mock>({ + state$: mock>(), + update: jest.fn((callback) => callback([], {} as any)), + }); + stateProvider = mock({ + getGlobal: jest.fn(() => globalStateMock), + }); + portMock = createPortSpyMock(BrowserTaskSchedulerPortName); + foregroundTaskSchedulerService = new ForegroundTaskSchedulerService(logService, stateProvider); + foregroundTaskSchedulerService["port"] = portMock; + foregroundTaskSchedulerService.registerTaskHandler( + ScheduledTaskNames.loginStrategySessionTimeout, + jest.fn(), + ); + jest.spyOn(globalThis, "setTimeout"); + jest.spyOn(globalThis, "setInterval"); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("sets a timeout for a task and sends a message to the background to set up a backup timeout alarm", async () => { + foregroundTaskSchedulerService.setTimeout(ScheduledTaskNames.loginStrategySessionTimeout, 1000); + await flushPromises(); + + expect(globalThis.setTimeout).toHaveBeenCalledWith(expect.any(Function), 1000); + expect(chrome.alarms.create).toHaveBeenCalledWith( + "loginStrategySessionTimeout", + { delayInMinutes: 0.5 }, + expect.any(Function), + ); + expect(portMock.postMessage).toHaveBeenCalledWith({ + action: BrowserTaskSchedulerPortActions.setTimeout, + taskName: ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs: 1000, + }); + }); + + it("sets an interval for a task and sends a message to the background to set up a backup interval alarm", async () => { + foregroundTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + 1000, + ); + await flushPromises(); + + expect(globalThis.setInterval).toHaveBeenCalledWith(expect.any(Function), 1000); + expect(portMock.postMessage).toHaveBeenCalledWith({ + action: BrowserTaskSchedulerPortActions.setInterval, + taskName: ScheduledTaskNames.loginStrategySessionTimeout, + intervalInMs: 1000, + }); + }); +}); diff --git a/apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.ts b/apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.ts new file mode 100644 index 00000000000..af4d56aa62a --- /dev/null +++ b/apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.ts @@ -0,0 +1,71 @@ +import { Subscription } from "rxjs"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { ScheduledTaskName } from "@bitwarden/common/platform/scheduling"; +import { StateProvider } from "@bitwarden/common/platform/state"; + +import { + BrowserTaskSchedulerPortActions, + BrowserTaskSchedulerPortMessage, + BrowserTaskSchedulerPortName, +} from "../abstractions/browser-task-scheduler.service"; + +import { BrowserTaskSchedulerServiceImplementation } from "./browser-task-scheduler.service"; + +export class ForegroundTaskSchedulerService extends BrowserTaskSchedulerServiceImplementation { + private port: chrome.runtime.Port; + + constructor(logService: LogService, stateProvider: StateProvider) { + super(logService, stateProvider); + + this.port = chrome.runtime.connect({ name: BrowserTaskSchedulerPortName }); + } + + /** + * Sends a port message to the background to set up a fallback timeout. Also sets a timeout locally. + * This is done to ensure that the timeout triggers even if the popup is closed. + * + * @param taskName - The name of the task. + * @param delayInMs - The delay in milliseconds. + */ + setTimeout(taskName: ScheduledTaskName, delayInMs: number): Subscription { + this.sendPortMessage({ + action: BrowserTaskSchedulerPortActions.setTimeout, + taskName, + delayInMs, + }); + + return super.setTimeout(taskName, delayInMs); + } + + /** + * Sends a port message to the background to set up a fallback interval. Also sets an interval locally. + * This is done to ensure that the interval triggers even if the popup is closed. + * + * @param taskName - The name of the task. + * @param intervalInMs - The interval in milliseconds. + * @param initialDelayInMs - The initial delay in milliseconds. + */ + setInterval( + taskName: ScheduledTaskName, + intervalInMs: number, + initialDelayInMs?: number, + ): Subscription { + this.sendPortMessage({ + action: BrowserTaskSchedulerPortActions.setInterval, + taskName, + intervalInMs, + }); + + return super.setInterval(taskName, intervalInMs, initialDelayInMs); + } + + /** + * Sends a message to the background task scheduler. + * + * @param message - The message to send. + */ + private sendPortMessage(message: BrowserTaskSchedulerPortMessage) { + this.port.postMessage(message); + } +} 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/platform/sync/foreground-sync.service.spec.ts b/apps/browser/src/platform/sync/foreground-sync.service.spec.ts index a9ee7c23b9c..365ce6a83ca 100644 --- a/apps/browser/src/platform/sync/foreground-sync.service.spec.ts +++ b/apps/browser/src/platform/sync/foreground-sync.service.spec.ts @@ -7,8 +7,11 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; @@ -18,6 +21,7 @@ import { DO_FULL_SYNC, ForegroundSyncService, FullSyncMessage } from "./foregrou import { FullSyncFinishedMessage } from "./sync-service.listener"; describe("ForegroundSyncService", () => { + const userId = Utils.newGuid() as UserId; const stateService = mock(); const folderService = mock(); const folderApiService = mock(); @@ -31,6 +35,7 @@ describe("ForegroundSyncService", () => { const sendService = mock(); const sendApiService = mock(); const messageListener = mock(); + const stateProvider = new FakeStateProvider(mockAccountServiceWith(userId)); const sut = new ForegroundSyncService( stateService, @@ -46,6 +51,7 @@ describe("ForegroundSyncService", () => { sendService, sendApiService, messageListener, + stateProvider, ); beforeEach(() => { diff --git a/apps/browser/src/platform/sync/foreground-sync.service.ts b/apps/browser/src/platform/sync/foreground-sync.service.ts index 0a2c7074298..23c0e1ff9f9 100644 --- a/apps/browser/src/platform/sync/foreground-sync.service.ts +++ b/apps/browser/src/platform/sync/foreground-sync.service.ts @@ -11,6 +11,7 @@ import { MessageSender, } from "@bitwarden/common/platform/messaging"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { StateProvider } from "@bitwarden/common/platform/state"; import { CoreSyncService } from "@bitwarden/common/platform/sync/internal"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { InternalSendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; @@ -40,6 +41,7 @@ export class ForegroundSyncService extends CoreSyncService { sendService: InternalSendService, sendApiService: SendApiService, private readonly messageListener: MessageListener, + stateProvider: StateProvider, ) { super( stateService, @@ -54,6 +56,7 @@ export class ForegroundSyncService extends CoreSyncService { authService, sendService, sendApiService, + stateProvider, ); } diff --git a/apps/browser/src/popup/app-routing.animations.ts b/apps/browser/src/popup/app-routing.animations.ts index 065331bd414..90990ea832d 100644 --- a/apps/browser/src/popup/app-routing.animations.ts +++ b/apps/browser/src/popup/app-routing.animations.ts @@ -177,6 +177,9 @@ export const routerTransition = trigger("routerTransition", [ transition("tabs => account-security", inSlideLeft), transition("account-security => tabs", outSlideRight), + transition("tabs => assign-collections", inSlideLeft), + transition("assign-collections => tabs", outSlideRight), + // Vault settings transition("tabs => vault-settings", inSlideLeft), transition("vault-settings => tabs", outSlideRight), @@ -196,6 +199,12 @@ export const routerTransition = trigger("routerTransition", [ transition("vault-settings => sync", inSlideLeft), transition("sync => vault-settings", outSlideRight), + transition("vault-settings => trash", inSlideLeft), + transition("trash => vault-settings", outSlideRight), + + transition("trash => view-cipher", inSlideLeft), + transition("view-cipher => trash", outSlideRight), + // Appearance settings transition("tabs => appearance", inSlideLeft), transition("appearance => tabs", outSlideRight), diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 4b28444f9ba..f715d38422d 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -2,13 +2,16 @@ import { Injectable, NgModule } from "@angular/core"; import { ActivatedRouteSnapshot, RouteReuseStrategy, RouterModule, Routes } from "@angular/router"; import { - AuthGuard, + authGuard, lockGuard, redirectGuard, tdeDecryptionRequiredGuard, unauthGuardFn, } from "@bitwarden/angular/auth/guards"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; +import { generatorSwap } from "@bitwarden/angular/tools/generator/generator-swap"; +import { extensionRefreshRedirect } from "@bitwarden/angular/utils/extension-refresh-redirect"; +import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, @@ -16,9 +19,11 @@ import { RegistrationStartComponent, RegistrationStartSecondaryComponent, RegistrationStartSecondaryComponentData, + SetPasswordJitComponent, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap"; import { fido2AuthGuard } from "../auth/guards/fido2-auth.guard"; import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component"; import { EnvironmentComponent } from "../auth/popup/environment.component"; @@ -33,19 +38,32 @@ import { RemovePasswordComponent } from "../auth/popup/remove-password.component import { SetPasswordComponent } from "../auth/popup/set-password.component"; import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; import { SsoComponent } from "../auth/popup/sso.component"; +import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component"; import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; +import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component"; +import { Fido2Component } from "../autofill/popup/fido2/fido2.component"; +import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; +import { ExcludedDomainsV1Component } from "../autofill/popup/settings/excluded-domains-v1.component"; import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component"; +import { NotificationsSettingsV1Component } from "../autofill/popup/settings/notifications-v1.component"; import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component"; +import { PremiumV2Component } from "../billing/popup/settings/premium-v2.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 { CredentialGeneratorHistoryComponent } from "../tools/popup/generator/credential-generator-history.component"; +import { CredentialGeneratorComponent } from "../tools/popup/generator/credential-generator.component"; 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"; import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component"; import { SendTypeComponent } from "../tools/popup/send/send-type.component"; +import { SendAddEditComponent as SendAddEditV2Component } from "../tools/popup/send-v2/add-edit/send-add-edit.component"; +import { SendCreatedComponent } from "../tools/popup/send-v2/send-created/send-created.component"; +import { SendV2Component } from "../tools/popup/send-v2/send-v2.component"; import { AboutPageV2Component } from "../tools/popup/settings/about-page/about-page-v2.component"; import { AboutPageComponent } from "../tools/popup/settings/about-page/about-page.component"; import { MoreFromBitwardenPageV2Component } from "../tools/popup/settings/about-page/more-from-bitwarden-page-v2.component"; @@ -56,7 +74,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 { Fido2Component } from "../vault/popup/components/fido2/fido2.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"; @@ -68,15 +86,19 @@ import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component"; import { ViewComponent } from "../vault/popup/components/vault/view.component"; import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/add-edit-v2.component"; +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 { TrashComponent } from "../vault/popup/settings/trash.component"; import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component"; import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component"; -import { extensionRefreshRedirect, extensionRefreshSwap } from "./extension-refresh-route-utils"; import { debounceNavigationGuard } from "./services/debounce-navigation.service"; import { TabsV2Component } from "./tabs-v2.component"; import { TabsComponent } from "./tabs.component"; @@ -93,6 +115,7 @@ const routes: Routes = [ pathMatch: "full", children: [], // Children lets us have an empty component. canActivate: [ + popupRouterCacheGuard, redirectGuard({ loggedIn: "/tabs/current", loggedOut: "/home", locked: "/lock" }), ], }, @@ -107,12 +130,11 @@ const routes: Routes = [ canActivate: [unauthGuardFn(unauthRouteOverrides)], data: { state: "home" }, }, - { + ...extensionRefreshSwap(Fido2V1Component, Fido2Component, { path: "fido2", - component: Fido2Component, canActivate: [fido2AuthGuard], data: { state: "fido2" }, - }, + }), { path: "login", component: LoginComponent, @@ -137,12 +159,26 @@ const routes: Routes = [ canActivate: [lockGuard()], data: { state: "lock", doNotSaveUrl: true }, }, - { - path: "2fa", - component: TwoFactorComponent, - canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { state: "2fa" }, - }, + ...twofactorRefactorSwap( + TwoFactorComponent, + AnonLayoutWrapperComponent, + { + path: "2fa", + canActivate: [unauthGuardFn(unauthRouteOverrides)], + data: { state: "2fa" }, + }, + { + path: "2fa", + canActivate: [unauthGuardFn(unauthRouteOverrides)], + data: { state: "2fa" }, + children: [ + { + path: "", + component: TwoFactorAuthComponent, + }, + ], + }, + ), { path: "2fa-options", component: TwoFactorOptionsComponent, @@ -168,7 +204,7 @@ const routes: Routes = [ { path: "remove-password", component: RemovePasswordComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "remove-password" }, }, { @@ -192,164 +228,161 @@ const routes: Routes = [ { path: "ciphers", component: VaultItemsComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "ciphers" }, }, - { + ...extensionRefreshSwap(ViewComponent, ViewV2Component, { path: "view-cipher", - component: ViewComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "view-cipher" }, - }, + }), { path: "cipher-password-history", component: PasswordHistoryComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "cipher-password-history" }, }, ...extensionRefreshSwap(AddEditComponent, AddEditV2Component, { path: "add-cipher", - canActivate: [AuthGuard, debounceNavigationGuard()], + canActivate: [authGuard, debounceNavigationGuard()], data: { state: "add-cipher" }, runGuardsAndResolvers: "always", }), ...extensionRefreshSwap(AddEditComponent, AddEditV2Component, { path: "edit-cipher", - canActivate: [AuthGuard, debounceNavigationGuard()], + canActivate: [authGuard, debounceNavigationGuard()], data: { state: "edit-cipher" }, runGuardsAndResolvers: "always", }), { path: "share-cipher", component: ShareComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "share-cipher" }, }, { path: "collections", component: CollectionsComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "collections" }, }, ...extensionRefreshSwap(AttachmentsComponent, AttachmentsV2Component, { path: "attachments", - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "attachments" }, }), { path: "generator", component: GeneratorComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "generator" }, }, - { + ...extensionRefreshSwap(PasswordGeneratorHistoryComponent, CredentialGeneratorHistoryComponent, { path: "generator-history", - component: PasswordGeneratorHistoryComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "generator-history" }, - }, + }), ...extensionRefreshSwap(ImportBrowserComponent, ImportBrowserV2Component, { path: "import", - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "import" }, }), ...extensionRefreshSwap(ExportBrowserComponent, ExportBrowserV2Component, { path: "export", - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "export" }, }), - { + ...extensionRefreshSwap(AutofillV1Component, AutofillComponent, { path: "autofill", - component: AutofillComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "autofill" }, - }, + }), { path: "account-security", component: AccountSecurityComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "account-security" }, }, - { + ...extensionRefreshSwap(NotificationsSettingsV1Component, NotificationsSettingsComponent, { path: "notifications", - component: NotificationsSettingsComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "notifications" }, - }, + }), ...extensionRefreshSwap(VaultSettingsComponent, VaultSettingsV2Component, { path: "vault-settings", - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "vault-settings" }, }), - { + ...extensionRefreshSwap(FoldersComponent, FoldersV2Component, { path: "folders", - component: FoldersComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "folders" }, - }, + }), { path: "add-folder", component: FolderAddEditComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "add-folder" }, }, { path: "edit-folder", component: FolderAddEditComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "edit-folder" }, }, { path: "sync", component: SyncComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "sync" }, }, - { + ...extensionRefreshSwap(ExcludedDomainsV1Component, ExcludedDomainsComponent, { path: "excluded-domains", - component: ExcludedDomainsComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "excluded-domains" }, - }, - { + }), + ...extensionRefreshSwap(PremiumComponent, PremiumV2Component, { path: "premium", component: PremiumComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "premium" }, - }, - { + }), + ...extensionRefreshSwap(AppearanceComponent, AppearanceV2Component, { path: "appearance", - component: AppearanceComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "appearance" }, - }, + }), ...extensionRefreshSwap(AddEditComponent, AddEditV2Component, { path: "clone-cipher", - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "clone-cipher" }, }), { path: "send-type", component: SendTypeComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "send-type" }, }, - { + ...extensionRefreshSwap(SendAddEditComponent, SendAddEditV2Component, { path: "add-send", - component: SendAddEditComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "add-send" }, - }, - { + }), + ...extensionRefreshSwap(SendAddEditComponent, SendAddEditV2Component, { path: "edit-send", - component: SendAddEditComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "edit-send" }, + }), + { + path: "send-created", + component: SendCreatedComponent, + canActivate: [authGuard], + data: { state: "send" }, }, { path: "update-temp-password", component: UpdateTempPasswordComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "update-temp-password" }, }, { @@ -389,16 +422,31 @@ const routes: Routes = [ }, ], }, + { + path: "set-password-jit", + canActivate: [canAccessFeature(FeatureFlag.EmailVerification)], + component: SetPasswordJitComponent, + data: { + pageTitle: "joinOrganization", + pageSubtitle: "finishJoiningThisOrganizationBySettingAMasterPassword", + } satisfies AnonLayoutWrapperData, + }, ], }, + { + path: "assign-collections", + component: AssignCollections, + canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh, true, "/")], + data: { state: "assign-collections" }, + }, ...extensionRefreshSwap(AboutPageComponent, AboutPageV2Component, { path: "about", - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "about" }, }), ...extensionRefreshSwap(MoreFromBitwardenPageComponent, MoreFromBitwardenPageV2Component, { path: "more-from-bitwarden", - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "moreFromBitwarden" }, }), ...extensionRefreshSwap(TabsComponent, TabsV2Component, { @@ -413,33 +461,32 @@ const routes: Routes = [ { path: "current", component: CurrentTabComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], canMatch: [extensionRefreshRedirect("/tabs/vault")], data: { state: "tabs_current" }, runGuardsAndResolvers: "always", }, ...extensionRefreshSwap(VaultFilterComponent, VaultV2Component, { path: "vault", - canActivate: [AuthGuard], + canActivate: [authGuard], + canDeactivate: [clearVaultStateGuard], data: { state: "tabs_vault" }, }), - { + ...generatorSwap(GeneratorComponent, CredentialGeneratorComponent, { path: "generator", - component: GeneratorComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "tabs_generator" }, - }, + }), ...extensionRefreshSwap(SettingsComponent, SettingsV2Component, { path: "settings", - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "tabs_settings" }, }), - { + ...extensionRefreshSwap(SendGroupingsComponent, SendV2Component, { path: "send", - component: SendGroupingsComponent, - canActivate: [AuthGuard], + canActivate: [authGuard], data: { state: "tabs_send" }, - }, + }), ], }), { @@ -447,6 +494,12 @@ const routes: Routes = [ component: AccountSwitcherComponent, data: { state: "account-switcher", doNotSaveUrl: true }, }, + { + path: "trash", + component: TrashComponent, + canActivate: [authGuard], + data: { state: "trash" }, + }, ]; @Injectable() diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index b70a5564ed9..477152fff85 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; +import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, inject } from "@angular/core"; import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap } from "rxjs"; @@ -6,8 +6,10 @@ import { LogoutReason } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { MessageListener } from "@bitwarden/common/platform/messaging"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -18,8 +20,8 @@ import { ToastService, } from "@bitwarden/components"; -import { BrowserApi } from "../platform/browser/browser-api"; -import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; +import { PopupViewCacheService } from "../platform/popup/view-cache/popup-view-cache.service"; +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"; @@ -35,9 +37,12 @@ import { DesktopSyncVerificationDialogComponent } from "./components/desktop-syn
`, }) export class AppComponent implements OnInit, OnDestroy { + private viewCacheService = inject(PopupViewCacheService); + private lastActivity: Date; private activeUserId: UserId; private recordActivitySubject = new Subject(); + private routerAnimations = false; private destroy$ = new Subject(); @@ -45,7 +50,7 @@ export class AppComponent implements OnInit, OnDestroy { private authService: AuthService, private i18nService: I18nService, private router: Router, - private stateService: BrowserStateService, + private stateService: StateService, private browserSendStateService: BrowserSendStateService, private vaultBrowserStateService: VaultBrowserStateService, private cipherService: CipherService, @@ -56,9 +61,13 @@ export class AppComponent implements OnInit, OnDestroy { private messageListener: MessageListener, private toastService: ToastService, private accountService: AccountService, + private animationControlService: AnimationControlService, ) {} async ngOnInit() { + initPopupClosedListener(); + await this.viewCacheService.init(); + // 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(); @@ -118,16 +127,6 @@ export class AppComponent implements OnInit, OnDestroy { this.showNativeMessagingFingerprintDialog(msg); } else if (msg.command === "showToast") { this.toastService._showToast(msg); - } else if (msg.command === "reloadProcess") { - const forceWindowReload = - this.platformUtilsService.isSafari() || - this.platformUtilsService.isFirefox() || - this.platformUtilsService.isOpera(); - // Wait to make sure background has reloaded first. - window.setTimeout( - () => BrowserApi.reloadExtension(forceWindowReload ? window : null), - 2000, - ); } else if (msg.command === "reloadPopup") { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -170,6 +169,12 @@ export class AppComponent implements OnInit, OnDestroy { } } }); + + this.animationControlService.enableRoutingAnimation$ + .pipe(takeUntil(this.destroy$)) + .subscribe((state) => { + this.routerAnimations = state; + }); } ngOnDestroy(): void { @@ -178,7 +183,9 @@ export class AppComponent implements OnInit, OnDestroy { } getState(outlet: RouterOutlet) { - if (outlet.activatedRouteData.state === "ciphers") { + if (!this.routerAnimations) { + return; + } else if (outlet.activatedRouteData.state === "ciphers") { const routeDirection = (window as any).routeDirection != null ? (window as any).routeDirection : ""; return ( @@ -240,7 +247,7 @@ export class AppComponent implements OnInit, OnDestroy { // Displaying toasts isn't super useful on the popup due to the reloads we do. // However, it is visible for a moment on the FF sidebar logout. private async displayLogoutReason(logoutReason: LogoutReason) { - let toastOptions: ToastOptions; + let toastOptions: ToastOptions | null = null; switch (logoutReason) { case "invalidSecurityStamp": case "sessionExpired": { @@ -253,6 +260,11 @@ export class AppComponent implements OnInit, OnDestroy { } } + if (toastOptions == null) { + // We don't have anything to show for this particular reason + return; + } + this.toastService.showToast(toastOptions); } } diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 3c7f45e55f7..f8d3c691051 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -17,7 +17,6 @@ import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe" import { UserVerificationDialogComponent } from "@bitwarden/auth/angular"; import { AvatarModule, ButtonModule, ToastModule } from "@bitwarden/components"; -import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component"; import { AccountComponent } from "../auth/popup/account-switching/account.component"; import { CurrentAccountComponent } from "../auth/popup/account-switching/current-account.component"; import { EnvironmentComponent } from "../auth/popup/environment.component"; @@ -36,8 +35,17 @@ import { SsoComponent } from "../auth/popup/sso.component"; import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; +import { Fido2CipherRowV1Component } from "../autofill/popup/fido2/fido2-cipher-row-v1.component"; +import { Fido2CipherRowComponent } from "../autofill/popup/fido2/fido2-cipher-row.component"; +import { Fido2UseBrowserLinkV1Component } from "../autofill/popup/fido2/fido2-use-browser-link-v1.component"; +import { Fido2UseBrowserLinkComponent } from "../autofill/popup/fido2/fido2-use-browser-link.component"; +import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component"; +import { Fido2Component } from "../autofill/popup/fido2/fido2.component"; +import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; +import { ExcludedDomainsV1Component } from "../autofill/popup/settings/excluded-domains-v1.component"; import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component"; +import { NotificationsSettingsV1Component } from "../autofill/popup/settings/notifications-v1.component"; import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component"; import { PremiumComponent } from "../billing/popup/settings/premium.component"; import { PopOutComponent } from "../platform/popup/components/pop-out.component"; @@ -56,9 +64,6 @@ import { SendTypeComponent } from "../tools/popup/send/send-type.component"; import { SettingsComponent } from "../tools/popup/settings/settings.component"; import { ActionButtonsComponent } from "../vault/popup/components/action-buttons.component"; import { CipherRowComponent } from "../vault/popup/components/cipher-row.component"; -import { Fido2CipherRowComponent } from "../vault/popup/components/fido2/fido2-cipher-row.component"; -import { Fido2UseBrowserLinkComponent } from "../vault/popup/components/fido2/fido2-use-browser-link.component"; -import { Fido2Component } from "../vault/popup/components/fido2/fido2.component"; import { AddEditCustomFieldsComponent } from "../vault/popup/components/vault/add-edit-custom-fields.component"; import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component"; import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component"; @@ -91,6 +96,7 @@ import "../platform/popup/locales"; imports: [ A11yModule, AppRoutingModule, + AutofillComponent, ToastModule.forRoot({ maxOpened: 2, autoDismiss: true, @@ -108,15 +114,21 @@ import "../platform/popup/locales"; ScrollingModule, ServicesModule, DialogModule, + ExcludedDomainsComponent, + Fido2CipherRowComponent, + Fido2Component, + Fido2UseBrowserLinkComponent, FilePopoutCalloutComponent, AvatarModule, AccountComponent, ButtonModule, + NotificationsSettingsComponent, PopOutComponent, PopupPageComponent, PopupTabNavigationComponent, PopupFooterComponent, PopupHeaderComponent, + HeaderComponent, UserVerificationDialogComponent, CurrentAccountComponent, ], @@ -133,20 +145,19 @@ import "../platform/popup/locales"; ColorPasswordCountPipe, CurrentTabComponent, EnvironmentComponent, - ExcludedDomainsComponent, - Fido2CipherRowComponent, - Fido2UseBrowserLinkComponent, + ExcludedDomainsV1Component, + Fido2CipherRowV1Component, + Fido2UseBrowserLinkV1Component, FolderAddEditComponent, FoldersComponent, VaultFilterComponent, - HeaderComponent, HintComponent, HomeComponent, LockComponent, LoginComponent, LoginViaAuthRequestComponent, LoginDecryptionOptionsComponent, - NotificationsSettingsComponent, + NotificationsSettingsV1Component, AppearanceComponent, GeneratorComponent, PasswordGeneratorHistoryComponent, @@ -175,11 +186,11 @@ import "../platform/popup/locales"; ViewCustomFieldsComponent, RemovePasswordComponent, VaultSelectComponent, - Fido2Component, - AutofillComponent, + Fido2V1Component, + AutofillV1Component, EnvironmentSelectorComponent, - AccountSwitcherComponent, ], + exports: [], providers: [CurrencyPipe, DatePipe], bootstrap: [AppComponent], }) diff --git a/apps/browser/src/popup/extension-refresh-route-utils.ts b/apps/browser/src/popup/extension-refresh-route-utils.ts deleted file mode 100644 index 3c2ca33f86e..00000000000 --- a/apps/browser/src/popup/extension-refresh-route-utils.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { inject, Type } from "@angular/core"; -import { Route, Router, Routes, UrlTree } from "@angular/router"; - -import { componentRouteSwap } from "@bitwarden/angular/utils/component-route-swap"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; - -/** - * Helper function to swap between two components based on the ExtensionRefresh feature flag. - * @param defaultComponent - The current non-refreshed component to render. - * @param refreshedComponent - The new refreshed component to render. - * @param options - The shared route options to apply to both components. - */ -export function extensionRefreshSwap( - defaultComponent: Type, - refreshedComponent: Type, - options: Route, -): Routes { - return componentRouteSwap( - defaultComponent, - refreshedComponent, - async () => { - const configService = inject(ConfigService); - return configService.getFeatureFlag(FeatureFlag.ExtensionRefresh); - }, - options, - ); -} - -/** - * Helper function to redirect to a new URL based on the ExtensionRefresh feature flag. - * @param redirectUrl - The URL to redirect to if the ExtensionRefresh flag is enabled. - */ -export function extensionRefreshRedirect(redirectUrl: string): () => Promise { - return async () => { - const configService = inject(ConfigService); - const router = inject(Router); - const shouldRedirect = await configService.getFeatureFlag(FeatureFlag.ExtensionRefresh); - if (shouldRedirect) { - return router.parseUrl(redirectUrl); - } else { - return true; - } - }; -} diff --git a/apps/browser/src/popup/images/two-factor/0.png b/apps/browser/src/popup/images/two-factor/0.png new file mode 100644 index 00000000000..307ff4fd60f Binary files /dev/null and b/apps/browser/src/popup/images/two-factor/0.png differ diff --git a/apps/browser/src/popup/images/two-factor/1-w.png b/apps/browser/src/popup/images/two-factor/1-w.png new file mode 100644 index 00000000000..a4e39b3f466 Binary files /dev/null and b/apps/browser/src/popup/images/two-factor/1-w.png differ diff --git a/apps/browser/src/popup/images/two-factor/1.png b/apps/browser/src/popup/images/two-factor/1.png new file mode 100644 index 00000000000..37fb7bc4327 Binary files /dev/null and b/apps/browser/src/popup/images/two-factor/1.png differ diff --git a/apps/browser/src/popup/images/two-factor/2.png b/apps/browser/src/popup/images/two-factor/2.png new file mode 100644 index 00000000000..d069bdab992 Binary files /dev/null and b/apps/browser/src/popup/images/two-factor/2.png differ diff --git a/apps/browser/src/popup/images/two-factor/3.png b/apps/browser/src/popup/images/two-factor/3.png new file mode 100644 index 00000000000..c543343f53b Binary files /dev/null and b/apps/browser/src/popup/images/two-factor/3.png differ diff --git a/apps/browser/src/popup/images/two-factor/4.png b/apps/browser/src/popup/images/two-factor/4.png new file mode 100644 index 00000000000..058671ea37e Binary files /dev/null and b/apps/browser/src/popup/images/two-factor/4.png differ diff --git a/apps/browser/src/popup/images/two-factor/6.png b/apps/browser/src/popup/images/two-factor/6.png new file mode 100644 index 00000000000..d069bdab992 Binary files /dev/null and b/apps/browser/src/popup/images/two-factor/6.png differ diff --git a/apps/browser/src/popup/images/two-factor/7-w.png b/apps/browser/src/popup/images/two-factor/7-w.png new file mode 100644 index 00000000000..89fdd8a2a08 Binary files /dev/null and b/apps/browser/src/popup/images/two-factor/7-w.png differ diff --git a/apps/browser/src/popup/images/two-factor/7.png b/apps/browser/src/popup/images/two-factor/7.png new file mode 100644 index 00000000000..2a38bdcd3e5 Binary files /dev/null and b/apps/browser/src/popup/images/two-factor/7.png differ diff --git a/apps/browser/src/popup/images/two-factor/rc-w.png b/apps/browser/src/popup/images/two-factor/rc-w.png new file mode 100644 index 00000000000..e83b8db1324 Binary files /dev/null and b/apps/browser/src/popup/images/two-factor/rc-w.png differ diff --git a/apps/browser/src/popup/images/two-factor/rc.png b/apps/browser/src/popup/images/two-factor/rc.png new file mode 100644 index 00000000000..4bebdf936cc Binary files /dev/null and b/apps/browser/src/popup/images/two-factor/rc.png differ diff --git a/apps/browser/src/popup/scss/misc.scss b/apps/browser/src/popup/scss/misc.scss index 134bac917d3..57bd3e010c8 100644 --- a/apps/browser/src/popup/scss/misc.scss +++ b/apps/browser/src/popup/scss/misc.scss @@ -287,102 +287,6 @@ app-vault-icon, cursor: move; } -.callout { - padding: 10px; - margin: 10px; - border: 1px solid #000000; - border-left-width: 5px; - border-radius: 3px; - @include themify($themes) { - border-color: themed("calloutBorderColor"); - background-color: themed("calloutBackgroundColor"); - } - - .callout-heading { - margin-top: 0; - } - - h3.callout-heading { - font-weight: bold; - text-transform: uppercase; - } - - &.callout-primary { - @include themify($themes) { - border-left-color: themed("primaryColor"); - } - - .callout-heading { - @include themify($themes) { - color: themed("primaryColor"); - } - } - } - - &.callout-info { - @include themify($themes) { - border-left-color: themed("infoColor"); - } - - .callout-heading { - @include themify($themes) { - color: themed("infoColor"); - } - } - } - - &.callout-danger { - @include themify($themes) { - border-left-color: themed("dangerColor"); - } - - .callout-heading { - @include themify($themes) { - color: themed("dangerColor"); - } - } - } - - &.callout-success { - @include themify($themes) { - border-left-color: themed("successColor"); - } - - .callout-heading { - @include themify($themes) { - color: themed("successColor"); - } - } - } - - &.callout-warning { - @include themify($themes) { - border-left-color: themed("warningColor"); - } - - .callout-heading { - @include themify($themes) { - color: themed("warningColor"); - } - } - } - - &.clickable { - &:hover, - &:focus, - &.active { - @include themify($themes) { - background-color: themed("boxBackgroundHoverColor"); - } - } - } - - .enforced-policy-options ul { - padding-left: 30px; - margin: 0; - } -} - input[type="password"]::-ms-reveal { display: none; } diff --git a/apps/browser/src/popup/scss/pages.scss b/apps/browser/src/popup/scss/pages.scss index 3ae36472996..bf8f03e7d03 100644 --- a/apps/browser/src/popup/scss/pages.scss +++ b/apps/browser/src/popup/scss/pages.scss @@ -217,7 +217,7 @@ app-vault-attachments { } } -app-fido2 { +app-fido2-v1 { .auth-wrapper { display: flex; flex-direction: column; diff --git a/apps/browser/src/popup/scss/plugins.scss b/apps/browser/src/popup/scss/plugins.scss new file mode 100644 index 00000000000..b8ac8697b7f --- /dev/null +++ b/apps/browser/src/popup/scss/plugins.scss @@ -0,0 +1,31 @@ +@import "variables.scss"; + +@each $mfaType in $mfaTypes { + .mfaType#{$mfaType} { + content: url("../images/two-factor/" + $mfaType + ".png"); + max-width: 100px; + } +} + +.mfaType1 { + @include themify($themes) { + content: url("../images/two-factor/1" + themed("mfaLogoSuffix")); + max-width: 100px; + max-height: 45px; + } +} + +.mfaType7 { + @include themify($themes) { + content: url("../images/two-factor/7" + themed("mfaLogoSuffix")); + max-width: 100px; + } +} + +.recovery-code-img { + @include themify($themes) { + content: url("../images/two-factor/rc" + themed("mfaLogoSuffix")); + max-width: 100px; + max-height: 45px; + } +} diff --git a/apps/browser/src/popup/scss/popup.scss b/apps/browser/src/popup/scss/popup.scss index 850ef96c64e..8dc2c206f87 100644 --- a/apps/browser/src/popup/scss/popup.scss +++ b/apps/browser/src/popup/scss/popup.scss @@ -10,5 +10,6 @@ @import "modal.scss"; @import "environment.scss"; @import "pages.scss"; +@import "plugins.scss"; @import "@angular/cdk/overlay-prebuilt.css"; @import "../../../../../libs/components/src/multi-select/scss/bw.theme"; diff --git a/apps/browser/src/popup/scss/variables.scss b/apps/browser/src/popup/scss/variables.scss index d8891cf620b..a4366a7415e 100644 --- a/apps/browser/src/popup/scss/variables.scss +++ b/apps/browser/src/popup/scss/variables.scss @@ -19,6 +19,8 @@ $border-radius: 6px; $line-height-base: 1.42857143; $icon-hover-color: lighten($text-color, 50%); +$mfaTypes: 0, 2, 3, 4, 6; + $gray: #555; $gray-light: #777; $text-muted: $gray-light; @@ -111,6 +113,7 @@ $themes: ( infoColor: $brand-info, warningColor: $brand-warning, logoSuffix: "dark", + mfaLogoSuffix: ".png", passwordNumberColor: #007fde, passwordSpecialColor: #c40800, passwordCountText: #212529, @@ -174,6 +177,7 @@ $themes: ( infoColor: #a4b0c6, warningColor: #ffeb66, logoSuffix: "white", + mfaLogoSuffix: "-w.png", passwordNumberColor: #6f9df1, passwordSpecialColor: #ff8d85, passwordCountText: #ffffff, @@ -236,6 +240,7 @@ $themes: ( infoColor: $nord9, warningColor: $nord12, logoSuffix: "white", + mfaLogoSuffix: "-w.png", passwordNumberColor: $nord8, passwordSpecialColor: $nord12, passwordCountText: $nord5, @@ -298,6 +303,7 @@ $themes: ( infoColor: $solarizedDarkGreen, warningColor: $solarizedDarkYellow, logoSuffix: "white", + mfaLogoSuffix: "-w.png", passwordNumberColor: $solarizedDarkCyan, passwordSpecialColor: $solarizedDarkYellow, passwordCountText: $solarizedDarkBase2, diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index 63ce45c9b76..9e6471eaf28 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -6,16 +6,16 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { BrowserApi } from "../../platform/browser/browser-api"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; -import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service"; @Injectable() export class InitService { constructor( private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, - private stateService: StateServiceAbstraction, + private stateService: StateService, private twoFactorService: TwoFactorService, private logService: LogServiceAbstraction, private themingService: AbstractThemingService, diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 7c187d00514..098c6eb91ce 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -1,8 +1,7 @@ import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core"; -import { Router } from "@angular/router"; import { Subject, merge, of } from "rxjs"; -import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards"; +import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service"; import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service"; import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { @@ -17,22 +16,17 @@ import { CLIENT_TYPE, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; -import { AuthRequestServiceAbstraction, PinServiceAbstraction } from "@bitwarden/auth/common"; +import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular"; +import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; -import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; -import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; -import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; -import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; -import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AutofillSettingsService, @@ -48,6 +42,11 @@ import { } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { ClientType } from "@bitwarden/common/enums"; +import { + AnimationControlService, + DefaultAnimationControlService, +} from "@bitwarden/common/platform/abstractions/animation-control.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -56,21 +55,21 @@ import { FileDownloadService } from "@bitwarden/common/platform/abstractions/fil import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; -import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; // eslint-disable-next-line no-restricted-imports -- Used for dependency injection import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; -import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; +import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; -import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { @@ -80,6 +79,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"; @@ -90,11 +91,10 @@ import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; -import { UnauthGuardService } from "../../auth/popup/services"; +import { ExtensionAnonLayoutWrapperDataService } from "../../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper-data.service"; import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service"; import AutofillService from "../../autofill/services/autofill.service"; import MainBackground from "../../background/main.background"; -import { Account } from "../../models/account"; import { BrowserApi } from "../../platform/browser/browser-api"; import { runInsideAngular } from "../../platform/browser/run-inside-angular.operator"; /* eslint-disable no-restricted-imports */ @@ -104,18 +104,20 @@ import { OffscreenDocumentService } from "../../platform/offscreen-document/abst import { DefaultOffscreenDocumentService } from "../../platform/offscreen-document/offscreen-document.service"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service"; -import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service"; +import { PopupViewCacheService } from "../../platform/popup/view-cache/popup-view-cache.service"; import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; import { BrowserCryptoService } from "../../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; -import { DefaultBrowserStateService } from "../../platform/services/default-browser-state.service"; +import { ForegroundBrowserBiometricsService } from "../../platform/services/foreground-browser-biometrics"; import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; +import { ForegroundTaskSchedulerService } from "../../platform/services/task-scheduler/foreground-task-scheduler.service"; import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging"; +import { ForegroundVaultTimeoutService } from "../../services/vault-timeout/foreground-vault-timeout.service"; import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service"; import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service"; import { Fido2UserVerificationService } from "../../vault/services/fido2-user-verification.service"; @@ -130,6 +132,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() @@ -170,16 +176,6 @@ const safeProviders: SafeProvider[] = [ deps: [InitService], multi: true, }), - safeProvider({ - provide: BaseUnauthGuardService, - useClass: UnauthGuardService, - deps: [AuthService, Router], - }), - safeProvider({ - provide: SsoLoginServiceAbstraction, - useFactory: getBgService("ssoLoginService"), - deps: [], - }), safeProvider({ provide: CryptoFunctionService, useFactory: () => new WebCryptoFunctionService(window), @@ -219,10 +215,11 @@ const safeProviders: SafeProvider[] = [ encryptService: EncryptService, platformUtilsService: PlatformUtilsService, logService: LogService, - stateService: StateServiceAbstraction, + stateService: StateService, accountService: AccountServiceAbstraction, stateProvider: StateProvider, biometricStateService: BiometricStateService, + biometricsService: BiometricsService, kdfConfigService: KdfConfigService, ) => { const cryptoService = new BrowserCryptoService( @@ -237,6 +234,7 @@ const safeProviders: SafeProvider[] = [ accountService, stateProvider, biometricStateService, + biometricsService, kdfConfigService, ); new ContainerService(cryptoService, encryptService).attachToGlobal(self); @@ -250,10 +248,11 @@ const safeProviders: SafeProvider[] = [ EncryptService, PlatformUtilsService, LogService, - StateServiceAbstraction, + StateService, AccountServiceAbstraction, StateProvider, BiometricStateService, + BiometricsService, KdfConfigService, ], }), @@ -262,21 +261,6 @@ const safeProviders: SafeProvider[] = [ useClass: TotpService, deps: [CryptoFunctionService, LogService], }), - safeProvider({ - provide: AuthRequestServiceAbstraction, - useFactory: getBgService("authRequestService"), - deps: [], - }), - safeProvider({ - provide: DeviceTrustServiceAbstraction, - useFactory: getBgService("deviceTrustService"), - deps: [], - }), - safeProvider({ - provide: DevicesServiceAbstraction, - useFactory: getBgService("devicesService"), - deps: [], - }), safeProvider({ provide: OffscreenDocumentService, useClass: DefaultOffscreenDocumentService, @@ -293,22 +277,19 @@ const safeProviders: SafeProvider[] = [ (clipboardValue: string, clearMs: number) => { void BrowserApi.sendMessage("clearClipboard", { clipboardValue, clearMs }); }, - async () => { - const response = await BrowserApi.sendMessageWithResponse<{ - result: boolean; - error: string; - }>("biometricUnlock"); - if (!response.result) { - throw response.error; - } - return response.result; - }, window, offscreenDocumentService, ); }, deps: [ToastService, OffscreenDocumentService], }), + safeProvider({ + provide: BiometricsService, + useFactory: () => { + return new ForegroundBrowserBiometricsService(); + }, + deps: [], + }), safeProvider({ provide: SyncService, useFactory: getBgService("syncService"), @@ -328,6 +309,11 @@ const safeProviders: SafeProvider[] = [ provide: AutofillServiceAbstraction, useExisting: AutofillService, }), + safeProvider({ + provide: ViewCacheService, + useExisting: PopupViewCacheService, + deps: [], + }), safeProvider({ provide: AutofillService, deps: [ @@ -342,6 +328,8 @@ const safeProviders: SafeProvider[] = [ ScriptInjectorService, AccountServiceAbstraction, AuthService, + ConfigService, + UserNotificationSettingsServiceAbstraction, MessageListener, ], }), @@ -350,25 +338,10 @@ const safeProviders: SafeProvider[] = [ useClass: BrowserScriptInjectorService, deps: [PlatformUtilsService, LogService], }), - safeProvider({ - provide: KeyConnectorService, - useFactory: getBgService("keyConnectorService"), - deps: [], - }), - safeProvider({ - provide: UserVerificationService, - useFactory: getBgService("userVerificationService"), - deps: [], - }), - safeProvider({ - provide: VaultTimeoutSettingsService, - useFactory: getBgService("vaultTimeoutSettingsService"), - deps: [], - }), safeProvider({ provide: VaultTimeoutService, - useFactory: getBgService("vaultTimeoutService"), - deps: [], + useClass: ForegroundVaultTimeoutService, + deps: [MessagingServiceAbstraction], }), safeProvider({ provide: NotificationsService, @@ -436,46 +409,6 @@ const safeProviders: SafeProvider[] = [ }, deps: [StateProvider], }), - safeProvider({ - provide: StateServiceAbstraction, - useFactory: ( - storageService: AbstractStorageService, - secureStorageService: AbstractStorageService, - memoryStorageService: AbstractStorageService, - logService: LogService, - accountService: AccountServiceAbstraction, - environmentService: EnvironmentService, - tokenService: TokenService, - migrationRunner: MigrationRunner, - ) => { - return new DefaultBrowserStateService( - storageService, - secureStorageService, - memoryStorageService, - logService, - new StateFactory(GlobalState, Account), - accountService, - environmentService, - tokenService, - migrationRunner, - ); - }, - deps: [ - AbstractStorageService, - SECURE_STORAGE, - MEMORY_STORAGE, - LogService, - AccountServiceAbstraction, - EnvironmentService, - TokenService, - MigrationRunner, - ], - }), - safeProvider({ - provide: BaseStateServiceAbstraction, - useExisting: StateServiceAbstraction, - deps: [], - }), safeProvider({ provide: FileDownloadService, useClass: BrowserFileDownloadService, @@ -583,6 +516,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, @@ -590,6 +529,7 @@ const safeProviders: SafeProvider[] = [ OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE, OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE, + DISK_BACKUP_LOCAL_STORAGE, ], }), safeProvider({ @@ -601,6 +541,25 @@ const safeProviders: SafeProvider[] = [ useClass: Fido2UserVerificationService, deps: [PasswordRepromptService, UserVerificationService, DialogService], }), + safeProvider({ + provide: AnimationControlService, + useClass: DefaultAnimationControlService, + deps: [GlobalStateProvider], + }), + safeProvider({ + provide: TaskSchedulerService, + useExisting: ForegroundTaskSchedulerService, + }), + safeProvider({ + provide: ForegroundTaskSchedulerService, + useFactory: getBgService("taskSchedulerService"), + deps: [], + }), + safeProvider({ + provide: AnonLayoutWrapperDataService, + useClass: ExtensionAnonLayoutWrapperDataService, + deps: [], + }), ]; @NgModule({ diff --git a/apps/browser/src/safari/mv3/fake-background.html b/apps/browser/src/safari/mv3/fake-background.html new file mode 100644 index 00000000000..afc33653c0a --- /dev/null +++ b/apps/browser/src/safari/mv3/fake-background.html @@ -0,0 +1,4 @@ + diff --git a/apps/browser/src/safari/mv3/fake-vendor.js b/apps/browser/src/safari/mv3/fake-vendor.js new file mode 100644 index 00000000000..a915519c38f --- /dev/null +++ b/apps/browser/src/safari/mv3/fake-vendor.js @@ -0,0 +1,2 @@ +// Empty file set for the Safari build process for mv3 to ensure backwards compatibility with mv2. +// Will be removed once we fully migrate Safari to mv3. diff --git a/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift b/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift index 95369453409..b0688e3bebb 100644 --- a/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift +++ b/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift @@ -133,12 +133,6 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(fallbackName.utf8.count), fallbackName, &passwordLength, &passwordPtr, nil) } - // TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3473) - if status != errSecSuccess { - let secondaryFallbackName = "_masterkey_biometric" - status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(secondaryFallbackName.utf8.count), secondaryFallbackName, &passwordLength, &passwordPtr, nil) - } - if status == errSecSuccess { let result = NSString(bytes: passwordPtr!, length: Int(passwordLength), encoding: String.Encoding.utf8.rawValue) as String? SecKeychainItemFreeContent(nil, passwordPtr) diff --git a/apps/browser/src/services/vault-timeout/foreground-vault-timeout.service.ts b/apps/browser/src/services/vault-timeout/foreground-vault-timeout.service.ts new file mode 100644 index 00000000000..462e2149e88 --- /dev/null +++ b/apps/browser/src/services/vault-timeout/foreground-vault-timeout.service.ts @@ -0,0 +1,18 @@ +import { VaultTimeoutService as BaseVaultTimeoutService } from "@bitwarden/common/src/abstractions/vault-timeout/vault-timeout.service"; +import { MessagingService } from "@bitwarden/common/src/platform/abstractions/messaging.service"; +import { UserId } from "@bitwarden/common/src/types/guid"; + +export class ForegroundVaultTimeoutService implements BaseVaultTimeoutService { + constructor(protected messagingService: MessagingService) {} + + // should only ever run in background + async checkVaultTimeout(): Promise {} + + async lock(userId?: UserId): Promise { + this.messagingService.send("lockVault", { userId }); + } + + async logOut(userId?: string): Promise { + this.messagingService.send("logout", { userId }); + } +} diff --git a/apps/browser/src/services/vault-timeout/vault-timeout.service.ts b/apps/browser/src/services/vault-timeout/vault-timeout.service.ts index 9e9a24fb9c3..e0b9db5422b 100644 --- a/apps/browser/src/services/vault-timeout/vault-timeout.service.ts +++ b/apps/browser/src/services/vault-timeout/vault-timeout.service.ts @@ -4,16 +4,13 @@ import { SafariApp } from "../../browser/safariApp"; export default class VaultTimeoutService extends BaseVaultTimeoutService { startCheck() { - // 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.checkVaultTimeout(); if (this.platformUtilsService.isSafari()) { - // 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.checkSafari(); - } else { - setInterval(() => this.checkVaultTimeout(), 10 * 1000); // check every 10 seconds + this.checkVaultTimeout().catch((error) => this.logService.error(error)); + this.checkSafari().catch((error) => this.logService.error(error)); + return; } + + super.startCheck(); } // This is a work-around to safari adding an arbitrary delay to setTimeout and diff --git a/apps/browser/src/tools/popup/generator/credential-generator-history.component.html b/apps/browser/src/tools/popup/generator/credential-generator-history.component.html new file mode 100644 index 00000000000..48f0479d8d6 --- /dev/null +++ b/apps/browser/src/tools/popup/generator/credential-generator-history.component.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/apps/browser/src/tools/popup/generator/credential-generator-history.component.ts b/apps/browser/src/tools/popup/generator/credential-generator-history.component.ts new file mode 100644 index 00000000000..0de71cf5b98 --- /dev/null +++ b/apps/browser/src/tools/popup/generator/credential-generator-history.component.ts @@ -0,0 +1,66 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { BehaviorSubject, distinctUntilChanged, firstValueFrom, map, switchMap } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { ButtonModule, ContainerComponent } from "@bitwarden/components"; +import { + CredentialGeneratorHistoryComponent as CredentialGeneratorHistoryToolsComponent, + EmptyCredentialHistoryComponent, +} from "@bitwarden/generator-components"; +import { GeneratorHistoryService } from "@bitwarden/generator-history"; + +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"; + +@Component({ + selector: "app-credential-generator-history", + templateUrl: "credential-generator-history.component.html", + standalone: true, + imports: [ + ButtonModule, + CommonModule, + ContainerComponent, + JslibModule, + PopOutComponent, + PopupHeaderComponent, + PopupPageComponent, + CredentialGeneratorHistoryToolsComponent, + EmptyCredentialHistoryComponent, + PopupFooterComponent, + ], +}) +export class CredentialGeneratorHistoryComponent { + protected readonly hasHistory$ = new BehaviorSubject(false); + protected readonly userId$ = new BehaviorSubject(null); + + constructor( + private accountService: AccountService, + private history: GeneratorHistoryService, + ) { + this.accountService.activeAccount$ + .pipe( + takeUntilDestroyed(), + map(({ id }) => id), + distinctUntilChanged(), + ) + .subscribe(this.userId$); + + this.userId$ + .pipe( + takeUntilDestroyed(), + switchMap((id) => id && this.history.credentials$(id)), + map((credentials) => credentials.length > 0), + ) + .subscribe(this.hasHistory$); + } + + clear = async () => { + await this.history.clear(await firstValueFrom(this.userId$)); + }; +} diff --git a/apps/browser/src/tools/popup/generator/credential-generator.component.html b/apps/browser/src/tools/popup/generator/credential-generator.component.html new file mode 100644 index 00000000000..d8c49da5b1a --- /dev/null +++ b/apps/browser/src/tools/popup/generator/credential-generator.component.html @@ -0,0 +1 @@ + diff --git a/apps/browser/src/tools/popup/generator/credential-generator.component.ts b/apps/browser/src/tools/popup/generator/credential-generator.component.ts new file mode 100644 index 00000000000..16938fbe79f --- /dev/null +++ b/apps/browser/src/tools/popup/generator/credential-generator.component.ts @@ -0,0 +1,11 @@ +import { Component } from "@angular/core"; + +import { PasswordGeneratorComponent } from "@bitwarden/generator-components"; + +@Component({ + standalone: true, + selector: "credential-generator", + templateUrl: "credential-generator.component.html", + imports: [PasswordGeneratorComponent], +}) +export class CredentialGeneratorComponent {} diff --git a/apps/browser/src/tools/popup/generator/generator.component.html b/apps/browser/src/tools/popup/generator/generator.component.html index 4c39c22b488..d92d32a5623 100644 --- a/apps/browser/src/tools/popup/generator/generator.component.html +++ b/apps/browser/src/tools/popup/generator/generator.component.html @@ -240,7 +240,7 @@ />
- + + + + + + + + + + diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts new file mode 100644 index 00000000000..48e6cbb8a31 --- /dev/null +++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts @@ -0,0 +1,141 @@ +import { CommonModule, Location } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormsModule } from "@angular/forms"; +import { ActivatedRoute, Params } from "@angular/router"; +import { map, switchMap } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { SendId } from "@bitwarden/common/types/guid"; +import { AsyncActionsModule, ButtonModule, SearchModule } from "@bitwarden/components"; +import { + DefaultSendFormConfigService, + SendFormConfig, + SendFormConfigService, + SendFormMode, +} from "@bitwarden/send-ui"; + +import { SendFormModule } from "../../../../../../../libs/tools/send/send-ui/src/send-form/send-form.module"; +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"; + +/** + * Helper class to parse query parameters for the AddEdit route. + */ +class QueryParams { + constructor(params: Params) { + this.sendId = params.sendId; + this.type = parseInt(params.type, 10); + } + + /** + * The ID of the send to edit, empty when it's a new Send + */ + sendId?: SendId; + + /** + * The type of send to create. + */ + type: SendType; +} + +export type AddEditQueryParams = Partial>; + +/** + * Component for adding or editing a send item. + */ +@Component({ + selector: "tools-send-add-edit", + templateUrl: "send-add-edit.component.html", + standalone: true, + providers: [{ provide: SendFormConfigService, useClass: DefaultSendFormConfigService }], + imports: [ + CommonModule, + SearchModule, + JslibModule, + FormsModule, + ButtonModule, + PopupPageComponent, + PopupHeaderComponent, + PopupFooterComponent, + SendFormModule, + AsyncActionsModule, + ], +}) +export class SendAddEditComponent { + /** + * The header text for the component. + */ + headerText: string; + + /** + * The configuration for the send form. + */ + config: SendFormConfig; + + constructor( + private route: ActivatedRoute, + private location: Location, + private i18nService: I18nService, + private addEditFormConfigService: SendFormConfigService, + ) { + this.subscribeToParams(); + } + + /** + * Handles the event when the send is saved. + */ + onSendSaved() { + this.location.back(); + } + + /** + * Subscribes to the route query parameters and builds the configuration based on the parameters. + */ + subscribeToParams(): void { + this.route.queryParams + .pipe( + takeUntilDestroyed(), + map((params) => new QueryParams(params)), + switchMap(async (params) => { + let mode: SendFormMode; + if (params.sendId == null) { + mode = "add"; + } else { + mode = "edit"; + } + const config = await this.addEditFormConfigService.buildConfig( + mode, + params.sendId, + params.type, + ); + return config; + }), + ) + .subscribe((config) => { + this.config = config; + this.headerText = this.getHeaderText(config.mode, config.sendType); + }); + } + + /** + * Gets the header text based on the mode and type. + * @param mode The mode of the send form. + * @param type The type of the send form. + * @returns The header text. + */ + private getHeaderText(mode: SendFormMode, type: SendType) { + const headerKey = + mode === "edit" || mode === "partial-edit" ? "editItemHeader" : "newItemHeader"; + + switch (type) { + case SendType.Text: + return this.i18nService.t(headerKey, this.i18nService.t("sendTypeText")); + case SendType.File: + return this.i18nService.t(headerKey, this.i18nService.t("sendTypeFile")); + } + } +} diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html new file mode 100644 index 00000000000..9b56fa74d91 --- /dev/null +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html @@ -0,0 +1,28 @@ +
+ + + + + + + +
+ +

{{ "createdSendSuccessfully" | i18n }}

+

{{ "sendAvailability" | i18n: daysAvailable }}

+ +
+ + + + +
+
diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts new file mode 100644 index 00000000000..413f22565e1 --- /dev/null +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts @@ -0,0 +1,140 @@ +import { CommonModule, Location } from "@angular/common"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ActivatedRoute, RouterLink } from "@angular/router"; +import { RouterTestingModule } from "@angular/router/testing"; +import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; +import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; +import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { ButtonModule, IconModule, ToastService } from "@bitwarden/components"; + +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 { PopupRouterCacheService } from "../../../../platform/popup/view-cache/popup-router-cache.service"; + +import { SendCreatedComponent } from "./send-created.component"; + +describe("SendCreatedComponent", () => { + let component: SendCreatedComponent; + let fixture: ComponentFixture; + let i18nService: MockProxy; + let platformUtilsService: MockProxy; + let sendService: MockProxy; + let toastService: MockProxy; + let location: MockProxy; + let activatedRoute: MockProxy; + let environmentService: MockProxy; + + const sendId = "test-send-id"; + const deletionDate = new Date(); + deletionDate.setDate(deletionDate.getDate() + 7); + const sendView: SendView = { + id: sendId, + deletionDate, + accessId: "abc", + urlB64Key: "123", + } as SendView; + + beforeEach(async () => { + i18nService = mock(); + platformUtilsService = mock(); + sendService = mock(); + toastService = mock(); + location = mock(); + activatedRoute = mock(); + environmentService = mock(); + Object.defineProperty(environmentService, "environment$", { + configurable: true, + get: () => of(new SelfHostedEnvironment({ webVault: "https://example.com" })), + }); + + activatedRoute.snapshot = { + queryParamMap: { + get: jest.fn().mockReturnValue(sendId), + }, + } as any; + + sendService.sendViews$ = of([sendView]); + + await TestBed.configureTestingModule({ + imports: [ + CommonModule, + RouterTestingModule, + JslibModule, + ButtonModule, + IconModule, + PopOutComponent, + PopupHeaderComponent, + PopupPageComponent, + RouterLink, + PopupFooterComponent, + SendCreatedComponent, + ], + providers: [ + { provide: I18nService, useValue: i18nService }, + { provide: PlatformUtilsService, useValue: platformUtilsService }, + { provide: SendService, useValue: sendService }, + { provide: ToastService, useValue: toastService }, + { provide: Location, useValue: location }, + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: ConfigService, useValue: mock() }, + { provide: EnvironmentService, useValue: environmentService }, + { provide: PopupRouterCacheService, useValue: mock() }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SendCreatedComponent); + component = fixture.componentInstance; + }); + + it("should create", () => { + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it("should initialize send and daysAvailable", () => { + fixture.detectChanges(); + expect(component["send"]).toBe(sendView); + expect(component["daysAvailable"]).toBe(7); + }); + + it("should navigate back on close", () => { + fixture.detectChanges(); + component.close(); + expect(location.back).toHaveBeenCalled(); + }); + + describe("getDaysAvailable", () => { + it("returns the correct number of days", () => { + fixture.detectChanges(); + expect(component.getDaysAvailable(sendView)).toBe(7); + }); + }); + + describe("copyLink", () => { + it("should copy link and show toast", async () => { + fixture.detectChanges(); + const link = "https://example.com/#/send/abc/123"; + + await component.copyLink(); + + expect(platformUtilsService.copyToClipboard).toHaveBeenCalledWith(link); + expect(toastService.showToast).toHaveBeenCalledWith({ + variant: "success", + title: null, + message: i18nService.t("sendLinkCopied"), + }); + }); + }); +}); diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts new file mode 100644 index 00000000000..92339774d05 --- /dev/null +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts @@ -0,0 +1,82 @@ +import { CommonModule, Location } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ActivatedRoute, RouterLink } from "@angular/router"; +import { firstValueFrom } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; +import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { ButtonModule, IconModule, ToastService } from "@bitwarden/components"; +import { SendCreatedIcon } from "@bitwarden/send-ui"; + +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"; + +@Component({ + selector: "app-send-created", + templateUrl: "./send-created.component.html", + standalone: true, + imports: [ + ButtonModule, + CommonModule, + JslibModule, + PopOutComponent, + PopupHeaderComponent, + PopupPageComponent, + RouterLink, + PopupFooterComponent, + IconModule, + ], +}) +export class SendCreatedComponent { + protected sendCreatedIcon = SendCreatedIcon; + protected send: SendView; + protected daysAvailable = 0; + + constructor( + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + private sendService: SendService, + private route: ActivatedRoute, + private toastService: ToastService, + private location: Location, + private environmentService: EnvironmentService, + ) { + const sendId = this.route.snapshot.queryParamMap.get("sendId"); + this.sendService.sendViews$.pipe(takeUntilDestroyed()).subscribe((sendViews) => { + this.send = sendViews.find((s) => s.id === sendId); + if (this.send) { + this.daysAvailable = this.getDaysAvailable(this.send); + } + }); + } + + getDaysAvailable(send: SendView): number { + const now = new Date().getTime(); + return Math.max(0, Math.ceil((send.deletionDate.getTime() - now) / (1000 * 60 * 60 * 24))); + } + + close() { + this.location.back(); + } + + async copyLink() { + if (!this.send || !this.send.accessId || !this.send.urlB64Key) { + return; + } + const env = await firstValueFrom(this.environmentService.environment$); + const link = env.getSendUrl() + this.send.accessId + "/" + this.send.urlB64Key; + this.platformUtilsService.copyToClipboard(link); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("sendLinkCopied"), + }); + } +} diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.html b/apps/browser/src/tools/popup/send-v2/send-v2.component.html new file mode 100644 index 00000000000..a8dd3e24f29 --- /dev/null +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.html @@ -0,0 +1,39 @@ + + + + + + + + + + +
+ + {{ "sendsNoItemsTitle" | i18n }} + {{ "sendsNoItemsMessage" | i18n }} + + +
+ + +
+ + {{ "noItemsMatchSearch" | i18n }} + {{ "clearFiltersOrTryAnother" | i18n }} + +
+ +
+ +
+ + +
+
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 new file mode 100644 index 00000000000..50e5531743a --- /dev/null +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts @@ -0,0 +1,136 @@ +import { CommonModule } from "@angular/common"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; +import { RouterTestingModule } from "@angular/router/testing"; +import { MockProxy, mock } from "jest-mock-extended"; +import { of, BehaviorSubject } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; +import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; +import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; +import { ButtonModule, NoItemsModule } from "@bitwarden/components"; +import { + NewSendDropdownComponent, + SendListItemsContainerComponent, + SendItemsService, + SendSearchComponent, + SendListFiltersComponent, + SendListFiltersService, +} from "@bitwarden/send-ui"; + +import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component"; +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"; + +describe("SendV2Component", () => { + let component: SendV2Component; + let fixture: ComponentFixture; + let sendItemsService: MockProxy; + let sendListFiltersService: SendListFiltersService; + let sendListFiltersServiceFilters$: BehaviorSubject<{ sendType: SendType | null }>; + let sendItemsServiceEmptyList$: BehaviorSubject; + let sendItemsServiceNoFilteredResults$: BehaviorSubject; + + beforeEach(async () => { + sendListFiltersServiceFilters$ = new BehaviorSubject({ sendType: null }); + sendItemsServiceEmptyList$ = new BehaviorSubject(false); + sendItemsServiceNoFilteredResults$ = new BehaviorSubject(false); + + sendItemsService = mock({ + filteredAndSortedSends$: of([ + { id: "1", name: "Send A" }, + { id: "2", name: "Send B" }, + ] as SendView[]), + latestSearchText$: of(""), + }); + + sendListFiltersService = new SendListFiltersService(mock(), new FormBuilder()); + + sendListFiltersService.filters$ = sendListFiltersServiceFilters$; + sendItemsService.emptyList$ = sendItemsServiceEmptyList$; + sendItemsService.noFilteredResults$ = sendItemsServiceNoFilteredResults$; + + await TestBed.configureTestingModule({ + imports: [ + CommonModule, + RouterTestingModule, + JslibModule, + ReactiveFormsModule, + ButtonModule, + NoItemsModule, + NewSendDropdownComponent, + SendListItemsContainerComponent, + SendListFiltersComponent, + SendSearchComponent, + SendV2Component, + PopupPageComponent, + PopupHeaderComponent, + PopOutComponent, + CurrentAccountComponent, + ], + providers: [ + { provide: AccountService, useValue: mock() }, + { provide: AuthService, useValue: mock() }, + { provide: AvatarService, useValue: mock() }, + { + provide: BillingAccountProfileStateService, + useValue: { hasPremiumFromAnySource$: of(false) }, + }, + { provide: ConfigService, useValue: mock() }, + { provide: EnvironmentService, useValue: mock() }, + { provide: LogService, useValue: mock() }, + { provide: PlatformUtilsService, useValue: mock() }, + { provide: SendApiService, useValue: mock() }, + { provide: SendItemsService, useValue: mock() }, + { provide: SearchService, useValue: mock() }, + { provide: SendService, useValue: { sendViews$: new BehaviorSubject([]) } }, + { provide: SendItemsService, useValue: sendItemsService }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: SendListFiltersService, useValue: sendListFiltersService }, + { provide: PopupRouterCacheService, useValue: mock() }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(SendV2Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); + + it("should update the title based on the current filter", () => { + sendListFiltersServiceFilters$.next({ sendType: SendType.File }); + fixture.detectChanges(); + expect(component["title"]).toBe("fileSends"); + }); + + it("should set listState to Empty when emptyList$ emits true", () => { + sendItemsServiceEmptyList$.next(true); + fixture.detectChanges(); + expect(component["listState"]).toBe(SendState.Empty); + }); + + it("should set listState to NoResults when noFilteredResults$ emits true", () => { + sendItemsServiceNoFilteredResults$.next(true); + fixture.detectChanges(); + expect(component["listState"]).toBe(SendState.NoResults); + }); +}); diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts new file mode 100644 index 00000000000..5c1ec89fde9 --- /dev/null +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.ts @@ -0,0 +1,98 @@ +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 } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { ButtonModule, Icons, NoItemsModule } from "@bitwarden/components"; +import { + NoSendsIcon, + NewSendDropdownComponent, + SendListItemsContainerComponent, + SendItemsService, + SendSearchComponent, + SendListFiltersComponent, + SendListFiltersService, +} from "@bitwarden/send-ui"; + +import { CurrentAccountComponent } from "../../../auth/popup/account-switching/current-account.component"; +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"; + +export enum SendState { + Empty, + NoResults, +} + +@Component({ + templateUrl: "send-v2.component.html", + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + PopOutComponent, + CurrentAccountComponent, + NoItemsModule, + JslibModule, + CommonModule, + ButtonModule, + RouterLink, + NewSendDropdownComponent, + SendListItemsContainerComponent, + SendListFiltersComponent, + SendSearchComponent, + ], +}) +export class SendV2Component implements OnInit, OnDestroy { + sendType = SendType; + + sendState = SendState; + + protected listState: SendState | null = null; + + protected sends$ = this.sendItemsService.filteredAndSortedSends$; + + protected title: string = "allSends"; + + protected noItemIcon = NoSendsIcon; + + protected noResultsIcon = Icons.NoResults; + + constructor( + protected sendItemsService: SendItemsService, + protected sendListFiltersService: SendListFiltersService, + ) { + combineLatest([ + this.sendItemsService.emptyList$, + this.sendItemsService.noFilteredResults$, + this.sendListFiltersService.filters$, + ]) + .pipe(takeUntilDestroyed()) + .subscribe(([emptyList, noFilteredResults, currentFilter]) => { + if (currentFilter?.sendType !== null) { + this.title = `${this.sendType[currentFilter.sendType].toLowerCase()}Sends`; + } else { + this.title = "allSends"; + } + + if (emptyList) { + this.listState = SendState.Empty; + return; + } + + if (noFilteredResults) { + this.listState = SendState.NoResults; + return; + } + + this.listState = null; + }); + } + + ngOnInit(): void {} + + ngOnDestroy(): void {} +} diff --git a/apps/browser/src/tools/popup/send/send-add-edit.component.ts b/apps/browser/src/tools/popup/send/send-add-edit.component.ts index 7f172178163..569a9a15a5e 100644 --- a/apps/browser/src/tools/popup/send/send-add-edit.component.ts +++ b/apps/browser/src/tools/popup/send/send-add-edit.component.ts @@ -1,5 +1,5 @@ import { DatePipe, Location } from "@angular/common"; -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { first } from "rxjs/operators"; @@ -13,12 +13,12 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { DialogService, ToastService } from "@bitwarden/components"; import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; -import { BrowserStateService } from "../../../platform/services/abstractions/browser-state.service"; import { FilePopoutUtilsService } from "../services/file-popout-utils.service"; @Component({ @@ -26,7 +26,7 @@ import { FilePopoutUtilsService } from "../services/file-popout-utils.service"; templateUrl: "send-add-edit.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class SendAddEditComponent extends BaseAddEditComponent { +export class SendAddEditComponent extends BaseAddEditComponent implements OnInit { // Options header showOptions = false; // File visibility @@ -37,7 +37,7 @@ export class SendAddEditComponent extends BaseAddEditComponent { constructor( i18nService: I18nService, platformUtilsService: PlatformUtilsService, - stateService: BrowserStateService, + stateService: StateService, messagingService: MessagingService, policyService: PolicyService, environmentService: EnvironmentService, diff --git a/apps/browser/src/tools/popup/send/send-groupings.component.ts b/apps/browser/src/tools/popup/send/send-groupings.component.ts index ae76c0ef94f..87de6af31cf 100644 --- a/apps/browser/src/tools/popup/send/send-groupings.component.ts +++ b/apps/browser/src/tools/popup/send/send-groupings.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, NgZone } from "@angular/core"; +import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { Router } from "@angular/router"; import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component"; @@ -26,7 +26,7 @@ const ComponentId = "SendComponent"; selector: "app-send-groupings", templateUrl: "send-groupings.component.html", }) -export class SendGroupingsComponent extends BaseSendComponent { +export class SendGroupingsComponent extends BaseSendComponent implements OnInit, OnDestroy { // Header showLeftHeader = true; // State Handling @@ -64,7 +64,7 @@ export class SendGroupingsComponent extends BaseSendComponent { dialogService, toastService, ); - super.onSuccessfulLoad = async () => { + this.onSuccessfulLoad = async () => { this.selectAll(); }; } diff --git a/apps/browser/src/tools/popup/send/send-type.component.ts b/apps/browser/src/tools/popup/send/send-type.component.ts index 122aa7e021d..8329831e0bc 100644 --- a/apps/browser/src/tools/popup/send/send-type.component.ts +++ b/apps/browser/src/tools/popup/send/send-type.component.ts @@ -1,5 +1,5 @@ import { Location } from "@angular/common"; -import { ChangeDetectorRef, Component, NgZone } from "@angular/core"; +import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { first } from "rxjs/operators"; @@ -27,7 +27,7 @@ const ComponentId = "SendTypeComponent"; selector: "app-send-type", templateUrl: "send-type.component.html", }) -export class SendTypeComponent extends BaseSendComponent { +export class SendTypeComponent extends BaseSendComponent implements OnInit, OnDestroy { groupingTitle: string; // State Handling state: BrowserComponentState; @@ -66,7 +66,7 @@ export class SendTypeComponent extends BaseSendComponent { dialogService, toastService, ); - super.onSuccessfulLoad = async () => { + this.onSuccessfulLoad = async () => { this.selectType(this.type); }; this.applySavedState = diff --git a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.html b/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.html index affe9ffc04e..9322ab5113e 100644 --- a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.html +++ b/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.html @@ -12,7 +12,7 @@ - + + + + +
+ + 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.html b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html index 4d8461a57c3..a46f5a6955b 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html @@ -1,11 +1,19 @@ - + + + {}); + +describe("AddEditV2Component", () => { + let component: AddEditV2Component; + let fixture: ComponentFixture; + let addEditCipherInfo$: BehaviorSubject; + let cipherServiceMock: MockProxy; + + const buildConfigResponse = { originalCipher: {} } as CipherFormConfig; + const buildConfig = jest.fn((mode: CipherFormMode) => + Promise.resolve({ mode, ...buildConfigResponse }), + ); + const queryParams$ = new BehaviorSubject({}); + const disable = jest.fn(); + const navigate = jest.fn(); + const back = jest.fn().mockResolvedValue(null); + + beforeEach(async () => { + buildConfig.mockClear(); + disable.mockClear(); + navigate.mockClear(); + back.mockClear(); + + addEditCipherInfo$ = new BehaviorSubject(null); + cipherServiceMock = mock(); + cipherServiceMock.addEditCipherInfo$ = addEditCipherInfo$.asObservable(); + + await TestBed.configureTestingModule({ + imports: [AddEditV2Component], + providers: [ + { provide: PlatformUtilsService, useValue: mock() }, + { provide: ConfigService, useValue: mock() }, + { provide: PopupRouterCacheService, useValue: { back } }, + { provide: PopupCloseWarningService, useValue: { disable } }, + { provide: Router, useValue: { navigate } }, + { provide: ActivatedRoute, useValue: { queryParams: queryParams$ } }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: CipherService, useValue: cipherServiceMock }, + ], + }) + .overrideProvider(CipherFormConfigService, { + useValue: { + buildConfig, + }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(AddEditV2Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe("query params", () => { + describe("mode", () => { + it("sets mode to `add` when no `cipherId` is provided", fakeAsync(() => { + queryParams$.next({}); + + tick(); + + expect(buildConfig.mock.lastCall[0]).toBe("add"); + expect(component.config.mode).toBe("add"); + })); + + it("sets mode to `edit` when `params.clone` is not provided", fakeAsync(() => { + queryParams$.next({ cipherId: "222-333-444-5555", clone: "true" }); + + tick(); + + expect(buildConfig.mock.lastCall[0]).toBe("clone"); + expect(component.config.mode).toBe("clone"); + })); + + it("sets mode to `edit` when `params.clone` is not provided", fakeAsync(() => { + buildConfigResponse.originalCipher = { edit: true } as Cipher; + queryParams$.next({ cipherId: "222-333-444-5555" }); + + tick(); + + expect(buildConfig.mock.lastCall[0]).toBe("edit"); + expect(component.config.mode).toBe("edit"); + })); + + it("sets mode to `partial-edit` when `config.originalCipher.edit` is false", fakeAsync(() => { + buildConfigResponse.originalCipher = { edit: false } as Cipher; + queryParams$.next({ cipherId: "222-333-444-5555" }); + + tick(); + + expect(buildConfig.mock.lastCall[0]).toBe("edit"); + expect(component.config.mode).toBe("partial-edit"); + })); + }); + }); + + describe("addEditCipherInfo initialization", () => { + it("populates config.initialValues with `addEditCipherInfo` values", fakeAsync(() => { + const addEditCipherInfo = { + cipher: { + name: "test", + folderId: "folder1", + organizationId: "org1", + type: CipherType.Login, + login: { + password: "password", + username: "username", + uris: [{ uri: "https://example.com" }], + }, + }, + collectionIds: ["col1", "col2"], + } as AddEditCipherInfo; + addEditCipherInfo$.next(addEditCipherInfo); + queryParams$.next({}); + + tick(); + + expect(component.config.initialValues).toEqual({ + name: "test", + folderId: "folder1", + organizationId: "org1", + password: "password", + username: "username", + loginUri: "https://example.com", + collectionIds: ["col1", "col2"], + } as OptionalInitialValues); + })); + + it("populates config.initialValues.username when `addEditCipherInfo` is an Identity", fakeAsync(() => { + addEditCipherInfo$.next({ + cipher: { type: CipherType.Identity, identity: { username: "identity-username" } }, + } as AddEditCipherInfo); + queryParams$.next({}); + + tick(); + + expect(component.config.initialValues.username).toBe("identity-username"); + })); + + it("overrides query params with `addEditCipherInfo` values", fakeAsync(() => { + addEditCipherInfo$.next({ + cipher: { name: "AddEditCipherName" }, + } as AddEditCipherInfo); + queryParams$.next({ + name: "QueryParamName", + }); + + tick(); + + expect(component.config.initialValues.name).toBe("AddEditCipherName"); + })); + + it("clears `addEditCipherInfo` after initialization", fakeAsync(() => { + addEditCipherInfo$.next({ cipher: { name: "test" } } as AddEditCipherInfo); + queryParams$.next({}); + + tick(); + + expect(cipherServiceMock.setAddEditCipherInfo).toHaveBeenCalledTimes(1); + })); + }); + + describe("onCipherSaved", () => { + it("disables warning when in popout", async () => { + jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValueOnce(true); + + await component.onCipherSaved({ id: "123-456-789" } as CipherView); + + expect(disable).toHaveBeenCalled(); + }); + + it("calls `confirmNewCredentialResponse` when in fido2 popout", async () => { + // @ts-expect-error - `inFido2PopoutWindow` is a private getter, mock the response here + // for the test rather than setting up the dependencies. + jest.spyOn(component, "inFido2PopoutWindow", "get").mockReturnValueOnce(true); + + await component.onCipherSaved({ id: "123-456-789" } as CipherView); + + expect(BrowserPopupUtils.inPopout).toHaveBeenCalled(); + expect(navigate).not.toHaveBeenCalled(); + }); + + it("closes single action popout", async () => { + jest.spyOn(BrowserPopupUtils, "inSingleActionPopout").mockReturnValueOnce(true); + jest.spyOn(BrowserPopupUtils, "closeSingleActionPopout").mockResolvedValue(); + + await component.onCipherSaved({ id: "123-456-789" } as CipherView); + + expect(BrowserPopupUtils.closeSingleActionPopout).toHaveBeenCalled(); + expect(navigate).not.toHaveBeenCalled(); + }); + + it("navigates to view-cipher for new ciphers", async () => { + component.config.mode = "add"; + + await component.onCipherSaved({ id: "123-456-789" } as CipherView); + + expect(navigate).toHaveBeenCalledWith(["/view-cipher"], { + replaceUrl: true, + queryParams: { cipherId: "123-456-789" }, + }); + expect(back).not.toHaveBeenCalled(); + }); + + it("navigates to view-cipher for edit ciphers", async () => { + component.config.mode = "edit"; + + await component.onCipherSaved({ id: "123-456-789" } as CipherView); + + expect(navigate).not.toHaveBeenCalled(); + expect(back).toHaveBeenCalled(); + }); + }); + + describe("handleBackButton", () => { + it("disables warning and aborts fido2 popout", async () => { + // @ts-expect-error - `inFido2PopoutWindow` is a private getter, mock the response here + // for the test rather than setting up the dependencies. + jest.spyOn(component, "inFido2PopoutWindow", "get").mockReturnValueOnce(true); + jest.spyOn(BrowserFido2UserInterfaceSession, "abortPopout"); + + await component.handleBackButton(); + + expect(disable).toHaveBeenCalled(); + expect(BrowserFido2UserInterfaceSession.abortPopout).toHaveBeenCalled(); + expect(back).not.toHaveBeenCalled(); + }); + + it("closes single action popout", async () => { + jest.spyOn(BrowserPopupUtils, "inSingleActionPopout").mockReturnValueOnce(true); + jest.spyOn(BrowserPopupUtils, "closeSingleActionPopout").mockResolvedValue(); + + await component.handleBackButton(); + + expect(BrowserPopupUtils.closeSingleActionPopout).toHaveBeenCalled(); + expect(back).not.toHaveBeenCalled(); + }); + + it("navigates the user backwards", async () => { + await component.handleBackButton(); + + expect(back).toHaveBeenCalled(); + }); + }); +}); 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 52f174f9e5e..7664c7e0ca1 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts @@ -1,27 +1,44 @@ -import { CommonModule, Location } from "@angular/common"; -import { Component } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { Component, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormsModule } from "@angular/forms"; -import { ActivatedRoute, Params } from "@angular/router"; -import { map, switchMap } from "rxjs"; +import { ActivatedRoute, Params, Router } from "@angular/router"; +import { firstValueFrom, map, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info"; import { AsyncActionsModule, ButtonModule, SearchModule } from "@bitwarden/components"; import { CipherFormConfig, CipherFormConfigService, + CipherFormGenerationService, CipherFormMode, CipherFormModule, DefaultCipherFormConfigService, + OptionalInitialValues, + 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 { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; +import { PopupCloseWarningService } from "../../../../../popup/services/popup-close-warning.service"; +import { BrowserCipherFormGenerationService } from "../../../services/browser-cipher-form-generation.service"; +import { BrowserTotpCaptureService } from "../../../services/browser-totp-capture.service"; +import { + fido2PopoutSessionData$, + Fido2SessionData, +} from "../../../utils/fido2-popout-session-data"; +import { VaultPopoutType } from "../../../utils/vault-popout-window"; import { OpenAttachmentsComponent } from "../attachments/open-attachments/open-attachments.component"; /** @@ -30,12 +47,14 @@ import { OpenAttachmentsComponent } from "../attachments/open-attachments/open-a class QueryParams { constructor(params: Params) { this.cipherId = params.cipherId; - this.type = parseInt(params.type, null); + this.type = params.type != undefined ? parseInt(params.type, null) : undefined; this.clone = params.clone === "true"; this.folderId = params.folderId; this.organizationId = params.organizationId; this.collectionId = params.collectionId; this.uri = params.uri; + this.username = params.username; + this.name = params.name; } /** @@ -46,7 +65,7 @@ class QueryParams { /** * The type of cipher to create. */ - type: CipherType; + type?: CipherType; /** * Whether to clone the cipher. @@ -72,6 +91,16 @@ class QueryParams { * Optional URI to pre-fill for login ciphers. */ uri?: string; + + /** + * Optional username to pre-fill for login/identity ciphers. + */ + username?: string; + + /** + * Optional name to pre-fill for the cipher. + */ + name?: string; } export type AddEditQueryParams = Partial>; @@ -80,7 +109,11 @@ export type AddEditQueryParams = Partial>; selector: "app-add-edit-v2", templateUrl: "add-edit-v2.component.html", standalone: true, - providers: [{ provide: CipherFormConfigService, useClass: DefaultCipherFormConfigService }], + providers: [ + { provide: CipherFormConfigService, useClass: DefaultCipherFormConfigService }, + { provide: TotpCaptureService, useClass: BrowserTotpCaptureService }, + { provide: CipherFormGenerationService, useClass: BrowserCipherFormGenerationService }, + ], imports: [ CommonModule, SearchModule, @@ -93,9 +126,10 @@ export type AddEditQueryParams = Partial>; PopupFooterComponent, CipherFormModule, AsyncActionsModule, + PopOutComponent, ], }) -export class AddEditV2Component { +export class AddEditV2Component implements OnInit { headerText: string; config: CipherFormConfig; @@ -107,17 +141,100 @@ export class AddEditV2Component { return this.config?.originalCipher?.id as CipherId; } + private fido2PopoutSessionData$ = fido2PopoutSessionData$(); + private fido2PopoutSessionData: Fido2SessionData; + + private get inFido2PopoutWindow() { + return BrowserPopupUtils.inPopout(window) && this.fido2PopoutSessionData.isFido2Session; + } + + private get inSingleActionPopout() { + return BrowserPopupUtils.inSingleActionPopout(window, VaultPopoutType.addEditVaultItem); + } + constructor( private route: ActivatedRoute, - private location: Location, private i18nService: I18nService, private addEditFormConfigService: CipherFormConfigService, + private popupCloseWarningService: PopupCloseWarningService, + private popupRouterCacheService: PopupRouterCacheService, + private router: Router, + private cipherService: CipherService, ) { this.subscribeToParams(); } - onCipherSaved(savedCipher: CipherView) { - this.location.back(); + async ngOnInit() { + this.fido2PopoutSessionData = await firstValueFrom(this.fido2PopoutSessionData$); + + if (BrowserPopupUtils.inPopout(window)) { + this.popupCloseWarningService.enable(); + } + } + + /** + * Called before the form is submitted, allowing us to handle Fido2 user verification. + */ + protected checkFido2UserVerification: () => Promise = async () => { + if (!this.inFido2PopoutWindow) { + // Not in a Fido2 popout window, no need to handle user verification. + return true; + } + + // TODO use fido2 user verification service once user verification for passkeys is approved for production. + // We are bypassing user verification pending approval for production. + return true; + }; + + /** + * Handle back button + */ + async handleBackButton() { + if (this.inFido2PopoutWindow) { + this.popupCloseWarningService.disable(); + BrowserFido2UserInterfaceSession.abortPopout(this.fido2PopoutSessionData.sessionId); + return; + } + + if (this.inSingleActionPopout) { + await BrowserPopupUtils.closeSingleActionPopout(VaultPopoutType.addEditVaultItem); + return; + } + + await this.popupRouterCacheService.back(); + } + + async onCipherSaved(cipher: CipherView) { + if (BrowserPopupUtils.inPopout(window)) { + this.popupCloseWarningService.disable(); + } + + if (this.inFido2PopoutWindow) { + BrowserFido2UserInterfaceSession.confirmNewCredentialResponse( + this.fido2PopoutSessionData.sessionId, + cipher.id, + this.fido2PopoutSessionData.userVerification, + ); + return; + } + + if (this.inSingleActionPopout) { + await BrowserPopupUtils.closeSingleActionPopout(VaultPopoutType.addEditVaultItem, 1000); + return; + } + + // When the cipher is in edit / partial edit, the previous page was the view-cipher page. + // In the case of creating a new cipher, the user should go view-cipher page but we need to also + // remove it from the history stack. This avoids the user having to click back twice on the + // view-cipher page. + if (this.config.mode === "edit" || this.config.mode === "partial-edit") { + await this.popupRouterCacheService.back(); + } else { + await this.router.navigate(["/view-cipher"], { + replaceUrl: true, + queryParams: { cipherId: cipher.id }, + }); + } } subscribeToParams(): void { @@ -142,7 +259,21 @@ export class AddEditV2Component { config.mode = "partial-edit"; } - this.setInitialValuesFromParams(params, config); + config.initialValues = this.setInitialValuesFromParams(params); + + // The browser notification bar and overlay use addEditCipherInfo$ to pass modified cipher details to the form + // Attempt to fetch them here and overwrite the initialValues if present + const cachedCipherInfo = await firstValueFrom(this.cipherService.addEditCipherInfo$); + + if (cachedCipherInfo != null) { + // Cached cipher info has priority over queryParams + config.initialValues = { + ...config.initialValues, + ...mapAddEditCipherInfoToInitialValues(cachedCipherInfo), + }; + // Be sure to clear the "cached" cipher info, so it doesn't get used again + await this.cipherService.setAddEditCipherInfo(null); + } return config; }), @@ -153,20 +284,27 @@ export class AddEditV2Component { }); } - setInitialValuesFromParams(params: QueryParams, config: CipherFormConfig) { - config.initialValues = {}; + setInitialValuesFromParams(params: QueryParams) { + const initialValues = {} as OptionalInitialValues; if (params.folderId) { - config.initialValues.folderId = params.folderId; + initialValues.folderId = params.folderId; } if (params.organizationId) { - config.initialValues.organizationId = params.organizationId; + initialValues.organizationId = params.organizationId; } if (params.collectionId) { - config.initialValues.collectionIds = [params.collectionId]; + initialValues.collectionIds = [params.collectionId]; } if (params.uri) { - config.initialValues.loginUri = params.uri; + initialValues.loginUri = params.uri; } + if (params.username) { + initialValues.username = params.username; + } + if (params.name) { + initialValues.name = params.name; + } + return initialValues; } setHeader(mode: CipherFormMode, type: CipherType) { @@ -174,13 +312,73 @@ export class AddEditV2Component { switch (type) { case CipherType.Login: - return this.i18nService.t(partOne, this.i18nService.t("typeLogin")); + return this.i18nService.t(partOne, this.i18nService.t("typeLogin").toLocaleLowerCase()); case CipherType.Card: - return this.i18nService.t(partOne, this.i18nService.t("typeCard")); + return this.i18nService.t(partOne, this.i18nService.t("typeCard").toLocaleLowerCase()); case CipherType.Identity: - return this.i18nService.t(partOne, this.i18nService.t("typeIdentity")); + return this.i18nService.t(partOne, this.i18nService.t("typeIdentity").toLocaleLowerCase()); case CipherType.SecureNote: - return this.i18nService.t(partOne, this.i18nService.t("note")); + return this.i18nService.t(partOne, this.i18nService.t("note").toLocaleLowerCase()); } } } + +/** + * Helper to map the old AddEditCipherInfo to the new OptionalInitialValues type used by the CipherForm + * @param cipherInfo + */ +const mapAddEditCipherInfoToInitialValues = ( + cipherInfo: AddEditCipherInfo | null, +): OptionalInitialValues => { + const initialValues: OptionalInitialValues = {}; + + if (cipherInfo == null) { + return initialValues; + } + + if (cipherInfo.collectionIds != null) { + initialValues.collectionIds = cipherInfo.collectionIds as CollectionId[]; + } + + if (cipherInfo.cipher == null) { + return initialValues; + } + + const cipher = cipherInfo.cipher; + + if (cipher.folderId != null) { + initialValues.folderId = cipher.folderId; + } + + if (cipher.organizationId != null) { + initialValues.organizationId = cipher.organizationId as OrganizationId; + } + + if (cipher.name != null) { + initialValues.name = cipher.name; + } + + if (cipher.type === CipherType.Login) { + const login = cipher.login; + + if (login != null) { + if (login.uris != null && login.uris.length > 0) { + initialValues.loginUri = login.uris[0].uri; + } + + if (login.username != null) { + initialValues.username = login.username; + } + + if (login.password != null) { + initialValues.password = login.password; + } + } + } + + if (cipher.type === CipherType.Identity && cipher.identity?.username != null) { + initialValues.username = cipher.identity.username; + } + + return initialValues; +}; diff --git a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.html b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.html new file mode 100644 index 00000000000..b0e651e8e2b --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + {{ "cancel" | i18n }} + + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts new file mode 100644 index 00000000000..8c8827336ea --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts @@ -0,0 +1,99 @@ +import { CommonModule, Location } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ReactiveFormsModule } from "@angular/forms"; +import { ActivatedRoute } from "@angular/router"; +import { Observable, combineLatest, first, map, switchMap } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +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 { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + ButtonModule, + CardComponent, + SelectModule, + FormFieldModule, + AsyncActionsModule, +} from "@bitwarden/components"; +import { AssignCollectionsComponent, CollectionAssignmentParams } from "@bitwarden/vault"; + +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"; + +@Component({ + standalone: true, + selector: "app-assign-collections", + templateUrl: "./assign-collections.component.html", + imports: [ + AsyncActionsModule, + ButtonModule, + CommonModule, + JslibModule, + SelectModule, + FormFieldModule, + AssignCollectionsComponent, + CardComponent, + ReactiveFormsModule, + PopupPageComponent, + PopupHeaderComponent, + PopupFooterComponent, + PopOutComponent, + ], +}) +export class AssignCollections { + /** Params needed to populate the assign collections component */ + params: CollectionAssignmentParams; + + constructor( + private location: Location, + private collectionService: CollectionService, + private cipherService: CipherService, + private accountService: AccountService, + route: ActivatedRoute, + ) { + const cipher$: Observable = route.queryParams.pipe( + switchMap(({ cipherId }) => this.cipherService.get(cipherId)), + switchMap((cipherDomain) => + this.accountService.activeAccount$.pipe( + map((account) => account?.id), + switchMap((userId) => + this.cipherService + .getKeyForCipherKeyDecryption(cipherDomain, userId) + .then(cipherDomain.decrypt.bind(cipherDomain)), + ), + ), + ), + ); + + combineLatest([cipher$, this.collectionService.decryptedCollections$]) + .pipe(takeUntilDestroyed(), first()) + .subscribe(([cipherView, collections]) => { + let availableCollections = collections.filter((c) => !c.readOnly); + const organizationId = (cipherView?.organizationId as OrganizationId) ?? null; + + // If the cipher is already a part of an organization, + // only show collections that belong to that organization + if (organizationId) { + availableCollections = availableCollections.filter( + (c) => c.organizationId === organizationId, + ); + } + + this.params = { + ciphers: [cipherView], + organizationId, + availableCollections, + }; + }); + } + + /** Navigates the user back to the previous screen */ + navigateBack() { + this.location.back(); + } +} diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.html index e7c59df21f4..58ad816bfc5 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.html @@ -7,7 +7,7 @@ *ngIf="cipherId" [cipherId]="cipherId" [submitBtn]="submitButton" - (onUploadSuccess)="navigateToEditScreen()" + (onUploadSuccess)="navigateBack()" > diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.spec.ts index 0f09d12db9f..29793a41ec9 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.spec.ts @@ -5,19 +5,23 @@ import { ActivatedRoute, Router } from "@angular/router"; import { mock } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherType } from "@bitwarden/common/vault/enums"; import { ButtonComponent } from "@bitwarden/components"; +import { CipherAttachmentsComponent } from "@bitwarden/vault"; import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component"; import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component"; +import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; import { AttachmentsV2Component } from "./attachments-v2.component"; -import { CipherAttachmentsComponent } from "./cipher-attachments/cipher-attachments.component"; @Component({ standalone: true, @@ -26,6 +30,7 @@ import { CipherAttachmentsComponent } from "./cipher-attachments/cipher-attachme }) class MockPopupHeaderComponent { @Input() pageTitle: string; + @Input() backAction: () => void; } @Component({ @@ -43,16 +48,13 @@ describe("AttachmentsV2Component", () => { const queryParams = new BehaviorSubject<{ cipherId: string }>({ cipherId: "5555-444-3333" }); let cipherAttachment: CipherAttachmentsComponent; const navigate = jest.fn(); + const back = jest.fn().mockResolvedValue(undefined); - const cipherDomain = { - type: CipherType.Login, - name: "Test Login", - }; - - const cipherServiceGet = jest.fn().mockResolvedValue(cipherDomain); + const mockUserId = Utils.newGuid() as UserId; + const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); beforeEach(async () => { - cipherServiceGet.mockClear(); + back.mockClear(); navigate.mockClear(); await TestBed.configureTestingModule({ @@ -61,6 +63,8 @@ describe("AttachmentsV2Component", () => { { provide: LogService, useValue: mock() }, { provide: ConfigService, useValue: mock() }, { provide: PlatformUtilsService, useValue: mock() }, + { provide: CipherService, useValue: mock() }, + { provide: PopupRouterCacheService, useValue: { back } }, { provide: I18nService, useValue: { t: (key: string) => key } }, { provide: Router, useValue: { navigate } }, { @@ -70,10 +74,8 @@ describe("AttachmentsV2Component", () => { }, }, { - provide: CipherService, - useValue: { - get: cipherServiceGet, - }, + provide: AccountService, + useValue: accountService, }, ], }) @@ -114,9 +116,6 @@ describe("AttachmentsV2Component", () => { tick(); - expect(navigate).toHaveBeenCalledWith(["/edit-cipher"], { - queryParams: { cipherId: "5555-444-3333", type: CipherType.Login }, - replaceUrl: true, - }); + expect(back).toHaveBeenCalledTimes(1); })); }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.ts index da0def529c2..09762767c81 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.ts @@ -1,20 +1,19 @@ import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { ActivatedRoute, Router } from "@angular/router"; +import { ActivatedRoute } from "@angular/router"; import { first } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { CipherId } from "@bitwarden/common/types/guid"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { ButtonModule } from "@bitwarden/components"; +import { CipherAttachmentsComponent } from "@bitwarden/vault"; 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 { CipherAttachmentsComponent } from "./cipher-attachments/cipher-attachments.component"; +import { PopupRouterCacheService } from "../../../../../platform/popup/view-cache/popup-router-cache.service"; @Component({ standalone: true, @@ -39,8 +38,7 @@ export class AttachmentsV2Component { cipherId: CipherId; constructor( - private router: Router, - private cipherService: CipherService, + private popupRouterCacheService: PopupRouterCacheService, route: ActivatedRoute, ) { route.queryParams.pipe(takeUntilDestroyed(), first()).subscribe(({ cipherId }) => { @@ -49,14 +47,7 @@ export class AttachmentsV2Component { } /** Navigate the user back to the edit screen after uploading an attachment */ - async navigateToEditScreen() { - const cipherDomain = await this.cipherService.get(this.cipherId); - - void this.router.navigate(["/edit-cipher"], { - queryParams: { cipherId: this.cipherId, type: cipherDomain.type }, - // "replaceUrl" so the /attachments route is not in the history, thus when a back button - // is clicked, the user is taken to the view screen instead of the attachments screen - replaceUrl: true, - }); + async navigateBack() { + await this.popupRouterCacheService.back(); } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts index a61c3eb6dd7..8c1e0641b03 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts @@ -5,16 +5,20 @@ import { BehaviorSubject } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { CipherId } from "@bitwarden/common/types/guid"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { ToastService } from "@bitwarden/components"; import BrowserPopupUtils from "../../../../../../platform/popup/browser-popup-utils"; +import { FilePopoutUtilsService } from "../../../../../../tools/popup/services/file-popout-utils.service"; import { OpenAttachmentsComponent } from "./open-attachments.component"; @@ -48,12 +52,17 @@ describe("OpenAttachmentsComponent", () => { const getCipher = jest.fn().mockResolvedValue(cipherDomain); const getOrganization = jest.fn().mockResolvedValue(org); + const showFilePopoutMessage = jest.fn().mockReturnValue(false); + + const mockUserId = Utils.newGuid() as UserId; + const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); beforeEach(async () => { openCurrentPagePopout.mockClear(); getCipher.mockClear(); showToast.mockClear(); getOrganization.mockClear(); + showFilePopoutMessage.mockClear(); await TestBed.configureTestingModule({ imports: [OpenAttachmentsComponent, RouterTestingModule], @@ -75,6 +84,14 @@ describe("OpenAttachmentsComponent", () => { provide: OrganizationService, useValue: { get: getOrganization }, }, + { + provide: FilePopoutUtilsService, + useValue: { showFilePopoutMessage }, + }, + { + provide: AccountService, + useValue: accountService, + }, ], }).compileComponents(); }); @@ -89,19 +106,22 @@ describe("OpenAttachmentsComponent", () => { }); it("opens attachments in new popout", async () => { - component.openAttachmentsInPopout = true; + showFilePopoutMessage.mockReturnValue(true); + + await component.ngOnInit(); await component.openAttachments(); - expect(router.navigate).not.toHaveBeenCalled(); - expect(openCurrentPagePopout).toHaveBeenCalledWith( - window, - "http:/localhost//attachments?cipherId=5555-444-3333", - ); + expect(router.navigate).toHaveBeenCalledWith(["/attachments"], { + queryParams: { cipherId: "5555-444-3333" }, + }); + expect(openCurrentPagePopout).toHaveBeenCalledWith(window); }); it("opens attachments in same window", async () => { - component.openAttachmentsInPopout = false; + showFilePopoutMessage.mockReturnValue(false); + + await component.ngOnInit(); await component.openAttachments(); diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts index c203620ed61..118510695c5 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts @@ -2,9 +2,11 @@ import { CommonModule } from "@angular/common"; import { Component, Input, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { Router } from "@angular/router"; +import { firstValueFrom, map } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -19,6 +21,7 @@ import { } from "@bitwarden/components"; import BrowserPopupUtils from "../../../../../../platform/popup/browser-popup-utils"; +import { FilePopoutUtilsService } from "../../../../../../tools/popup/services/file-popout-utils.service"; @Component({ standalone: true, @@ -31,7 +34,7 @@ export class OpenAttachmentsComponent implements OnInit { @Input({ required: true }) cipherId: CipherId; /** True when the attachments window should be opened in a popout */ - openAttachmentsInPopout = BrowserPopupUtils.inPopup(window); + openAttachmentsInPopout: boolean; /** True when the user has access to premium or h */ canAccessAttachments: boolean; @@ -46,6 +49,8 @@ export class OpenAttachmentsComponent implements OnInit { private organizationService: OrganizationService, private toastService: ToastService, private i18nService: I18nService, + private filePopoutUtilsService: FilePopoutUtilsService, + private accountService: AccountService, ) { this.billingAccountProfileStateService.hasPremiumFromAnySource$ .pipe(takeUntilDestroyed()) @@ -55,9 +60,18 @@ export class OpenAttachmentsComponent implements OnInit { } async ngOnInit(): Promise { + this.openAttachmentsInPopout = this.filePopoutUtilsService.showFilePopoutMessage(window); + + if (!this.cipherId) { + return; + } + const cipherDomain = await this.cipherService.get(this.cipherId); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); const cipher = await cipherDomain.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain), + await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId), ); if (!cipher.organizationId) { @@ -86,16 +100,13 @@ export class OpenAttachmentsComponent implements OnInit { return; } + await this.router.navigate(["/attachments"], { queryParams: { cipherId: this.cipherId } }); + + // Open the attachments page in a popout + // This is done after the router navigation to ensure that the navigation + // is included in the `PopupRouterCacheService` history if (this.openAttachmentsInPopout) { - const destinationUrl = this.router - .createUrlTree(["/attachments"], { queryParams: { cipherId: this.cipherId } }) - .toString(); - - const currentBaseUrl = window.location.href.replace(this.router.url, ""); - - await BrowserPopupUtils.openCurrentPagePopout(window, currentBaseUrl + destinationUrl); - } else { - await this.router.navigate(["/attachments"], { queryParams: { cipherId: this.cipherId } }); + await BrowserPopupUtils.openCurrentPagePopout(window); } } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html index 487168539b9..f4444a10aeb 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/item-copy-action/item-copy-actions.component.html @@ -13,7 +13,13 @@ - - + {{ "clone" | i18n }} - + + {{ "assignToCollections" | i18n }} + 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 23ff9593099..2bc3fcea2f9 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 @@ -1,8 +1,10 @@ import { CommonModule } from "@angular/common"; import { booleanAttribute, Component, Input } from "@angular/core"; import { Router, RouterModule } from "@angular/router"; +import { firstValueFrom, map } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; @@ -50,6 +52,7 @@ export class ItemMoreOptionsComponent { private router: Router, private i18nService: I18nService, private vaultPopupAutofillService: VaultPopupAutofillService, + private accountService: AccountService, ) {} get canEdit() { @@ -76,7 +79,7 @@ export class ItemMoreOptionsComponent { } async doAutofillAndSave() { - await this.vaultPopupAutofillService.doAutofillAndSave(this.cipher); + await this.vaultPopupAutofillService.doAutofillAndSave(this.cipher, false); } /** @@ -108,7 +111,10 @@ export class ItemMoreOptionsComponent { */ async toggleFavorite() { this.cipher.favorite = !this.cipher.favorite; - const encryptedCipher = await this.cipherService.encrypt(this.cipher); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const encryptedCipher = await this.cipherService.encrypt(this.cipher, activeUserId); await this.cipherService.updateWithServer(encryptedCipher); this.toastService.showToast({ variant: "success", @@ -152,4 +158,15 @@ export class ItemMoreOptionsComponent { } as AddEditQueryParams, }); } + + /** Prompts for password when necessary then navigates to the assign collections route */ + async conditionallyNavigateToAssignCollections() { + if (this.cipher.reprompt && !(await this.passwordRepromptService.showPasswordPrompt())) { + return; + } + + await this.router.navigate(["/assign-collections"], { + queryParams: { cipherId: this.cipher.id }, + }); + } } 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.html b/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.html index 32427ac2da6..0e241a81dcb 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-filters/vault-list-filters.component.html @@ -1,5 +1,5 @@
-
+ +

{{ title }} @@ -17,42 +17,50 @@ {{ description }}

- - - - {{ cipher.name }} - - {{ cipher.subTitle }} - - - - - - - - - + + + + + {{ cipher.name }} + + + {{ cipher.subTitle }} + + + + + + + + + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts index b6ba09fb315..615d37cb604 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts @@ -1,11 +1,14 @@ +import { ScrollingModule } from "@angular/cdk/scrolling"; import { CommonModule } from "@angular/common"; import { booleanAttribute, Component, EventEmitter, Input, Output } from "@angular/core"; -import { RouterLink } from "@angular/router"; +import { Router, RouterLink } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { BadgeModule, + BitItemHeight, + BitItemHeightClass, ButtonModule, IconButtonModule, ItemModule, @@ -13,6 +16,7 @@ import { SectionHeaderComponent, TypographyModule, } from "@bitwarden/components"; +import { OrgIconDirective, PasswordRepromptService } from "@bitwarden/vault"; import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; import { PopupCipherView } from "../../../views/popup-cipher.view"; @@ -33,12 +37,17 @@ import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options RouterLink, ItemCopyActionsComponent, ItemMoreOptionsComponent, + OrgIconDirective, + ScrollingModule, ], selector: "app-vault-list-items-container", templateUrl: "vault-list-items-container.component.html", standalone: true, }) export class VaultListItemsContainerComponent { + protected ItemHeightClass = BitItemHeightClass; + protected ItemHeight = BitItemHeight; + /** * The list of ciphers to display. */ @@ -76,6 +85,13 @@ export class VaultListItemsContainerComponent { @Input({ transform: booleanAttribute }) showAutofillButton: boolean; + /** + * Remove the bottom margin from the bit-section in this component + * (used for containers at the end of the page where bottom margin is not needed) + */ + @Input({ transform: booleanAttribute }) + disableSectionMargin: boolean = false; + /** * The tooltip text for the organization icon for ciphers that belong to an organization. * @param cipher @@ -91,9 +107,22 @@ export class VaultListItemsContainerComponent { constructor( private i18nService: I18nService, private vaultPopupAutofillService: VaultPopupAutofillService, + private passwordRepromptService: PasswordRepromptService, + private router: Router, ) {} async doAutofill(cipher: PopupCipherView) { await this.vaultPopupAutofillService.doAutofill(cipher); } + + async onViewCipher(cipher: PopupCipherView) { + const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher); + if (!repromptPassed) { + return; + } + + await this.router.navigate(["/view-cipher"], { + queryParams: { cipherId: cipher.id, type: cipher.type }, + }); + } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-ui-onboarding/vault-ui-onboarding.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-ui-onboarding/vault-ui-onboarding.component.ts new file mode 100644 index 00000000000..20b39c5a88d --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-ui-onboarding/vault-ui-onboarding.component.ts @@ -0,0 +1,79 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { + ButtonModule, + DialogModule, + DialogService, + IconModule, + svgIcon, +} from "@bitwarden/components"; + +const announcementIcon = svgIcon` + + + + + + + + + + + + + + + + +`; + +@Component({ + standalone: true, + selector: "app-vault-ui-onboarding", + template: ` + +
+ +
+ + {{ "bitwardenNewLook" | i18n }} + + + {{ "bitwardenNewLookDesc" | i18n }} + + + + + + +
+ `, + imports: [CommonModule, DialogModule, ButtonModule, JslibModule, IconModule], +}) +export class VaultUiOnboardingComponent { + icon = announcementIcon; + + static open(dialogService: DialogService) { + return dialogService.open(VaultUiOnboardingComponent); + } + + navigateToLink = async () => { + window.open( + "https://bitwarden.com/blog/bringing-intuitive-workflows-and-visual-updates-to-the-bitwarden-browser/", + "_blank", + ); + }; +} diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html new file mode 100644 index 00000000000..a778d6aaea9 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + 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..9851b16aa41 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts @@ -0,0 +1,126 @@ +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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; +import { 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 { VaultPopupAutofillService } from "./../../../services/vault-popup-autofill.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 mockVaultPopupAutofillService = { + doAutofill: jest.fn(), + }; + const mockUserId = Utils.newGuid() as UserId; + const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); + + 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; + }, + }, + }, + { provide: VaultPopupAutofillService, useValue: mockVaultPopupAutofillService }, + { + provide: AccountService, + useValue: accountService, + }, + ], + }).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 new file mode 100644 index 00000000000..1505d98ede2 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -0,0 +1,189 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormsModule } from "@angular/forms"; +import { ActivatedRoute, Router } from "@angular/router"; +import { firstValueFrom, map, Observable, switchMap } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AUTOFILL_ID, SHOW_AUTOFILL_BUTTON } from "@bitwarden/common/autofill/constants"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { + AsyncActionsModule, + SearchModule, + ButtonModule, + IconButtonModule, + 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"; +import { PopupPageComponent } from "./../../../../../platform/popup/layout/popup-page.component"; +import { VaultPopupAutofillService } from "./../../../services/vault-popup-autofill.service"; + +@Component({ + selector: "app-view-v2", + templateUrl: "view-v2.component.html", + standalone: true, + providers: [{ provide: TotpCaptureService, useClass: BrowserTotpCaptureService }], + imports: [ + CommonModule, + SearchModule, + JslibModule, + FormsModule, + ButtonModule, + PopupPageComponent, + PopupHeaderComponent, + PopupFooterComponent, + IconButtonModule, + CipherViewComponent, + AsyncActionsModule, + PopOutComponent, + ], +}) +export class ViewV2Component { + headerText: string; + cipher: CipherView; + organization$: Observable; + folder$: Observable; + collections$: Observable; + loadAction: typeof AUTOFILL_ID | typeof SHOW_AUTOFILL_BUTTON; + + constructor( + private route: ActivatedRoute, + private router: Router, + private i18nService: I18nService, + private cipherService: CipherService, + private dialogService: DialogService, + private logService: LogService, + private toastService: ToastService, + private vaultPopupAutofillService: VaultPopupAutofillService, + private accountService: AccountService, + ) { + this.subscribeToParams(); + } + + subscribeToParams(): void { + this.route.queryParams + .pipe( + switchMap(async (params): Promise => { + this.loadAction = params.action; + return await this.getCipherData(params.cipherId); + }), + switchMap(async (cipher) => { + this.cipher = cipher; + this.headerText = this.setHeader(cipher.type); + if (this.loadAction === AUTOFILL_ID || this.loadAction === SHOW_AUTOFILL_BUTTON) { + await this.vaultPopupAutofillService.doAutofill(this.cipher); + } + }), + takeUntilDestroyed(), + ) + .subscribe(); + } + + setHeader(type: CipherType) { + switch (type) { + case CipherType.Login: + return this.i18nService.t("viewItemHeader", this.i18nService.t("typeLogin").toLowerCase()); + case CipherType.Card: + return this.i18nService.t("viewItemHeader", this.i18nService.t("typeCard").toLowerCase()); + case CipherType.Identity: + return this.i18nService.t( + "viewItemHeader", + this.i18nService.t("typeIdentity").toLowerCase(), + ); + case CipherType.SecureNote: + return this.i18nService.t("viewItemHeader", this.i18nService.t("note").toLowerCase()); + } + } + + async getCipherData(id: string) { + const cipher = await this.cipherService.get(id); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + return await cipher.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), + ); + } + + async editCipher() { + if (this.cipher.isDeleted) { + return false; + } + void this.router.navigate(["/edit-cipher"], { + queryParams: { cipherId: this.cipher.id, type: this.cipher.type, isNew: false }, + }); + return true; + } + + delete = async (): Promise => { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "deleteItem" }, + content: { + key: this.cipher.isDeleted ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation", + }, + type: "warning", + }); + + if (!confirmed) { + return false; + } + + try { + await this.deleteCipher(); + } catch (e) { + this.logService.error(e); + return false; + } + + await this.router.navigate(["/vault"]); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t(this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem"), + }); + + return true; + }; + + restore = async (): Promise => { + try { + await this.cipherService.restoreWithServer(this.cipher.id); + } catch (e) { + this.logService.error(e); + } + + await this.router.navigate(["/vault"]); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("restoredItem"), + }); + }; + + protected deleteCipher() { + return this.cipher.isDeleted + ? this.cipherService.deleteWithServer(this.cipher.id) + : this.cipherService.softDeleteWithServer(this.cipher.id); + } + + protected showFooter(): boolean { + return this.cipher && (!this.cipher.isDeleted || (this.cipher.isDeleted && this.cipher.edit)); + } +} 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 e72077fa82d..02654f37efe 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 @@ -1,5 +1,5 @@ import { DatePipe, Location } from "@angular/common"; -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import qrcodeParser from "qrcode-parser"; import { firstValueFrom } from "rxjs"; @@ -23,13 +23,14 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; +import { normalizeExpiryYearFormat } from "@bitwarden/common/vault/utils"; 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"; @@ -39,7 +40,7 @@ import { closeAddEditVaultItemPopout, VaultPopoutType } from "../../utils/vault- templateUrl: "add-edit.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class AddEditComponent extends BaseAddEditComponent { +export class AddEditComponent extends BaseAddEditComponent implements OnInit { currentUris: string[]; showAttachments = true; openAttachmentsInPopup: boolean; @@ -141,6 +142,7 @@ export class AddEditComponent extends BaseAddEditComponent { } if ( params.uri && + this.cipher.login.uris[0] && (this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === "") ) { this.cipher.login.uris[0].uri = params.uri; @@ -181,6 +183,11 @@ export class AddEditComponent extends BaseAddEditComponent { const { isFido2Session, sessionId, userVerification } = fido2SessionData; const inFido2PopoutWindow = BrowserPopupUtils.inPopout(window) && isFido2Session; + // normalize card expiry year on save + if (this.cipher.type === this.cipherType.Card) { + this.cipher.card.expYear = normalizeExpiryYearFormat(this.cipher.card.expYear); + } + // TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production. // PM-4577 - https://github.com/bitwarden/clients/pull/8746 if ( @@ -396,6 +403,7 @@ export class AddEditComponent extends BaseAddEditComponent { } // TODO: Remove and use fido2 user verification service once user verification for passkeys is approved for production. + // Be sure to make the same changes to add-edit-v2.component.ts if applicable private async handleFido2UserVerification( sessionId: string, userVerification: boolean, diff --git a/apps/browser/src/vault/popup/components/vault/attachments.component.ts b/apps/browser/src/vault/popup/components/vault/attachments.component.ts index afe80cab8ce..75819689b44 100644 --- a/apps/browser/src/vault/popup/components/vault/attachments.component.ts +++ b/apps/browser/src/vault/popup/components/vault/attachments.component.ts @@ -1,10 +1,11 @@ import { Location } from "@angular/common"; -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { first } from "rxjs/operators"; import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; @@ -13,14 +14,14 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Component({ selector: "app-vault-attachments", templateUrl: "attachments.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class AttachmentsComponent extends BaseAttachmentsComponent { +export class AttachmentsComponent extends BaseAttachmentsComponent implements OnInit { openedAttachmentsInPopup: boolean; constructor( @@ -36,6 +37,8 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { fileDownloadService: FileDownloadService, dialogService: DialogService, billingAccountProfileStateService: BillingAccountProfileStateService, + accountService: AccountService, + toastService: ToastService, ) { super( cipherService, @@ -49,6 +52,8 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { fileDownloadService, dialogService, billingAccountProfileStateService, + accountService, + toastService, ); } diff --git a/apps/browser/src/vault/popup/components/vault/collections.component.ts b/apps/browser/src/vault/popup/components/vault/collections.component.ts index cb37f0fdad2..19d448e6033 100644 --- a/apps/browser/src/vault/popup/components/vault/collections.component.ts +++ b/apps/browser/src/vault/popup/components/vault/collections.component.ts @@ -1,23 +1,24 @@ import { Location } from "@angular/common"; -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { first } from "rxjs/operators"; import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-vault-collections", templateUrl: "collections.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class CollectionsComponent extends BaseCollectionsComponent { +export class CollectionsComponent extends BaseCollectionsComponent implements OnInit { constructor( collectionService: CollectionService, platformUtilsService: PlatformUtilsService, @@ -27,7 +28,8 @@ export class CollectionsComponent extends BaseCollectionsComponent { private route: ActivatedRoute, private location: Location, logService: LogService, - configService: ConfigService, + accountService: AccountService, + toastService: ToastService, ) { super( collectionService, @@ -36,7 +38,8 @@ export class CollectionsComponent extends BaseCollectionsComponent { cipherService, organizationService, logService, - configService, + accountService, + toastService, ); } diff --git a/apps/browser/src/vault/popup/components/vault/current-tab.component.html b/apps/browser/src/vault/popup/components/vault/current-tab.component.html index 0b2e16d09d2..bb8a401da62 100644 --- a/apps/browser/src/vault/popup/components/vault/current-tab.component.html +++ b/apps/browser/src/vault/popup/components/vault/current-tab.component.html @@ -36,44 +36,6 @@
- -

- {{ unassignedItemsBannerService.bannerText$ | async | i18n }} - {{ "unassignedItemsBannerCTAPartOne" | i18n }} - {{ "adminConsole" | i18n }} - {{ "unassignedItemsBannerCTAPartTwo" | i18n }} - {{ "learnMore" | i18n }} -

- -

{{ "typeLogins" | i18n }} diff --git a/apps/browser/src/vault/popup/components/vault/current-tab.component.ts b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts index 97856a952ce..ec69330745f 100644 --- a/apps/browser/src/vault/popup/components/vault/current-tab.component.ts +++ b/apps/browser/src/vault/popup/components/vault/current-tab.component.ts @@ -3,14 +3,11 @@ import { Router } from "@angular/router"; import { Subject, firstValueFrom, from, Subscription } from "rxjs"; import { debounceTime, switchMap, takeUntil } from "rxjs/operators"; -import { UnassignedItemsBannerService } from "@bitwarden/angular/services/unassigned-items-banner.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -58,10 +55,6 @@ export class CurrentTabComponent implements OnInit, OnDestroy { private loadedTimeout: number; private searchTimeout: number; - protected unassignedItemsBannerEnabled$ = this.configService.getFeatureFlag$( - FeatureFlag.UnassignedItemsBanner, - ); - constructor( private platformUtilsService: PlatformUtilsService, private cipherService: CipherService, @@ -78,8 +71,6 @@ export class CurrentTabComponent implements OnInit, OnDestroy { private organizationService: OrganizationService, private vaultFilterService: VaultFilterService, private vaultSettingsService: VaultSettingsService, - private configService: ConfigService, - protected unassignedItemsBannerService: UnassignedItemsBannerService, ) {} async ngOnInit() { diff --git a/apps/browser/src/vault/popup/components/vault/password-history.component.ts b/apps/browser/src/vault/popup/components/vault/password-history.component.ts index 05986aad51f..bf1b4ea7717 100644 --- a/apps/browser/src/vault/popup/components/vault/password-history.component.ts +++ b/apps/browser/src/vault/popup/components/vault/password-history.component.ts @@ -1,9 +1,10 @@ import { Location } from "@angular/common"; -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { first } from "rxjs/operators"; import { PasswordHistoryComponent as BasePasswordHistoryComponent } from "@bitwarden/angular/vault/components/password-history.component"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -13,15 +14,16 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi templateUrl: "password-history.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class PasswordHistoryComponent extends BasePasswordHistoryComponent { +export class PasswordHistoryComponent extends BasePasswordHistoryComponent implements OnInit { constructor( cipherService: CipherService, platformUtilsService: PlatformUtilsService, i18nService: I18nService, + accountService: AccountService, private location: Location, private route: ActivatedRoute, ) { - super(cipherService, platformUtilsService, i18nService, window); + super(cipherService, platformUtilsService, i18nService, accountService, window); } async ngOnInit() { diff --git a/apps/browser/src/vault/popup/components/vault/share.component.ts b/apps/browser/src/vault/popup/components/vault/share.component.ts index 1a6c9c059ec..44c0a24ab9b 100644 --- a/apps/browser/src/vault/popup/components/vault/share.component.ts +++ b/apps/browser/src/vault/popup/components/vault/share.component.ts @@ -1,9 +1,10 @@ -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { first } from "rxjs/operators"; import { ShareComponent as BaseShareComponent } from "@bitwarden/angular/components/share.component"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -15,7 +16,7 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti templateUrl: "share.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class ShareComponent extends BaseShareComponent { +export class ShareComponent extends BaseShareComponent implements OnInit { constructor( collectionService: CollectionService, platformUtilsService: PlatformUtilsService, @@ -25,6 +26,7 @@ export class ShareComponent extends BaseShareComponent { private route: ActivatedRoute, private router: Router, organizationService: OrganizationService, + accountService: AccountService, ) { super( collectionService, @@ -33,6 +35,7 @@ export class ShareComponent extends BaseShareComponent { cipherService, logService, organizationService, + accountService, ); } diff --git a/apps/browser/src/vault/popup/components/vault/vault-items.component.ts b/apps/browser/src/vault/popup/components/vault/vault-items.component.ts index abb810c04d5..df78806edf2 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-items.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-items.component.ts @@ -198,7 +198,9 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn super.selectCipher(cipher); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/view-cipher"], { queryParams: { cipherId: cipher.id } }); + this.router.navigate(["/view-cipher"], { + queryParams: { cipherId: cipher.id }, + }); } this.preventSelected = false; }, 200); 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.html b/apps/browser/src/vault/popup/components/vault/vault-v2.component.html index f666bc09a89..e402e131436 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault/vault-v2.component.html @@ -1,4 +1,4 @@ - + @@ -22,10 +22,13 @@

- + +
- +
+ +
- +
- +
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 777e44f0e16..c0886264875 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 @@ -1,20 +1,24 @@ +import { ScrollingModule } from "@angular/cdk/scrolling"; 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"; import { VaultPopupItemsService } from "../../services/vault-popup-items.service"; import { VaultPopupListFiltersService } from "../../services/vault-popup-list-filters.service"; +import { VaultUiOnboardingService } from "../../services/vault-ui-onboarding.service"; import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "../vault-v2"; import { NewItemDropdownV2Component, @@ -48,29 +52,37 @@ enum VaultState { RouterLink, VaultV2SearchComponent, NewItemDropdownV2Component, + ScrollingModule, ], + providers: [VaultUiOnboardingService], }) export class VaultV2Component implements OnInit, OnDestroy { cipherType = CipherType; + protected favoriteCiphers$ = this.vaultPopupItemsService.favoriteCiphers$; protected remainingCiphers$ = this.vaultPopupItemsService.remainingCiphers$; + protected loading$ = this.vaultPopupItemsService.loading$; 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; @@ -78,6 +90,7 @@ export class VaultV2Component implements OnInit, OnDestroy { constructor( private vaultPopupItemsService: VaultPopupItemsService, private vaultPopupListFiltersService: VaultPopupListFiltersService, + private vaultUiOnboardingService: VaultUiOnboardingService, ) { combineLatest([ this.vaultPopupItemsService.emptyVault$, @@ -103,7 +116,9 @@ export class VaultV2Component implements OnInit, OnDestroy { }); } - ngOnInit(): void {} + async ngOnInit() { + await this.vaultUiOnboardingService.showOnboardingDialog(); + } ngOnDestroy(): void {} } 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 ba101aa6536..f8e7de21dc8 100644 --- a/apps/browser/src/vault/popup/components/vault/view.component.ts +++ b/apps/browser/src/vault/popup/components/vault/view.component.ts @@ -1,13 +1,14 @@ import { DatePipe, Location } from "@angular/common"; -import { ChangeDetectorRef, Component, NgZone } from "@angular/core"; +import { ChangeDetectorRef, Component, NgZone, OnInit, OnDestroy } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { Subject, firstValueFrom, takeUntil, Subscription } from "rxjs"; -import { first } from "rxjs/operators"; +import { first, map } from "rxjs/operators"; import { ViewComponent as BaseViewComponent } from "@bitwarden/angular/vault/components/view.component"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; @@ -27,10 +28,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"; @@ -52,7 +53,7 @@ type LoadAction = typeof AUTOFILL_ID | typeof SHOW_AUTOFILL_BUTTON | CopyAction; selector: "app-vault-view", templateUrl: "view.component.html", }) -export class ViewComponent extends BaseViewComponent { +export class ViewComponent extends BaseViewComponent implements OnInit, OnDestroy { showAttachments = true; pageDetails: any[] = []; tab: any; @@ -97,6 +98,7 @@ export class ViewComponent extends BaseViewComponent { fileDownloadService: FileDownloadService, dialogService: DialogService, datePipe: DatePipe, + accountService: AccountService, billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( @@ -120,6 +122,7 @@ export class ViewComponent extends BaseViewComponent { fileDownloadService, dialogService, datePipe, + accountService, billingAccountProfileStateService, ); } @@ -267,7 +270,10 @@ export class ViewComponent extends BaseViewComponent { this.cipher.login.uris.push(loginUri); try { - const cipher: Cipher = await this.cipherService.encrypt(this.cipher); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const cipher: Cipher = await this.cipherService.encrypt(this.cipher, activeUserId); await this.cipherService.updateWithServer(cipher); this.platformUtilsService.showToast( "success", 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 new file mode 100644 index 00000000000..e790735dc52 --- /dev/null +++ b/apps/browser/src/vault/popup/services/browser-totp-capture.service.spec.ts @@ -0,0 +1,80 @@ +import { TestBed } from "@angular/core/testing"; +import qrcodeParser from "qrcode-parser"; + +import { BrowserApi } from "../../../platform/browser/browser-api"; + +import { BrowserTotpCaptureService } from "./browser-totp-capture.service"; + +jest.mock("qrcode-parser", () => jest.fn()); + +const mockQrcodeParser = qrcodeParser as jest.Mock; + +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"); + + testBed = TestBed.configureTestingModule({ + providers: [BrowserTotpCaptureService], + }); + service = testBed.inject(BrowserTotpCaptureService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); + + it("should call captureVisibleTab and qrcodeParser when captureTotpSecret is called", async () => { + mockQrcodeParser.mockResolvedValue({ toString: () => validTotpUrl }); + + await service.captureTotpSecret(); + + expect(mockCaptureVisibleTab).toHaveBeenCalled(); + expect(mockQrcodeParser).toHaveBeenCalledWith("screenshot"); + }); + + it("should return the totpUrl when captureTotpSecret is called", async () => { + mockQrcodeParser.mockResolvedValue({ toString: () => validTotpUrl }); + + const result = await service.captureTotpSecret(); + + expect(result).toEqual(validTotpUrl); + }); + + it("should return null when the URL is not the otpauth: protocol", async () => { + mockQrcodeParser.mockResolvedValue({ toString: () => "https://example.com" }); + + const result = await service.captureTotpSecret(); + + expect(result).toBeNull(); + }); + + it("should return null when the URL is missing the secret parameter", async () => { + mockQrcodeParser.mockResolvedValue({ toString: () => "otpauth://totp/label" }); + + const result = await service.captureTotpSecret(); + + 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 new file mode 100644 index 00000000000..8f93db45c0e --- /dev/null +++ b/apps/browser/src/vault/popup/services/browser-totp-capture.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from "@angular/core"; +import qrcodeParser from "qrcode-parser"; + +import { TotpCaptureService } from "@bitwarden/vault"; + +import { BrowserApi } from "../../../platform/browser/browser-api"; + +/** + * Implementation of TotpCaptureService for the browser which captures the + * TOTP secret from the currently visible tab. + */ +@Injectable() +export class BrowserTotpCaptureService implements TotpCaptureService { + async captureTotpSecret() { + const screenshot = await BrowserApi.captureVisibleTab(); + const data = await qrcodeParser(screenshot); + const url = new URL(data.toString()); + if (url.protocol === "otpauth:" && url.searchParams.has("secret")) { + return data.toString(); + } + return null; + } + + async openAutofillNewTab(loginUri: string) { + await BrowserApi.createNewTab(loginUri); + } +} diff --git a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts index 6e74fd7c231..effadad07fb 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts @@ -1,11 +1,15 @@ import { TestBed } from "@angular/core/testing"; +import { ActivatedRoute } from "@angular/router"; import { mock } from "jest-mock-extended"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, of } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { subscribeTo } from "@bitwarden/common/spec"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { FakeAccountService, mockAccountServiceWith, subscribeTo } from "@bitwarden/common/spec"; +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 { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; @@ -30,6 +34,9 @@ describe("VaultPopupAutofillService", () => { let service: VaultPopupAutofillService; const mockCurrentTab = { url: "https://example.com" } as chrome.tabs.Tab; + const mockActivatedRoute = { + queryParams: of({}), + } as any; // Create mocks for VaultPopupAutofillService const mockAutofillService = mock(); @@ -40,6 +47,9 @@ describe("VaultPopupAutofillService", () => { const mockCipherService = mock(); const mockMessagingService = mock(); + const mockUserId = Utils.newGuid() as UserId; + const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); + beforeEach(() => { jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false); jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(mockCurrentTab); @@ -55,6 +65,11 @@ describe("VaultPopupAutofillService", () => { { provide: PasswordRepromptService, useValue: mockPasswordRepromptService }, { provide: CipherService, useValue: mockCipherService }, { provide: MessagingService, useValue: mockMessagingService }, + { provide: ActivatedRoute, useValue: mockActivatedRoute }, + { + provide: AccountService, + useValue: accountService, + }, ], }); @@ -248,6 +263,17 @@ describe("VaultPopupAutofillService", () => { expect(setTimeout).toHaveBeenCalledTimes(1); expect(BrowserApi.closePopup).toHaveBeenCalled(); }); + + it("should show a successful toast message if login form is populated", async () => { + jest.spyOn(BrowserPopupUtils, "inSingleActionPopout").mockReturnValue(true); + (service as any).currentAutofillTab$ = of({ id: 1234 }); + await service.doAutofill(mockCipher); + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "success", + title: null, + message: mockI18nService.t("autoFillSuccess"), + }); + }); }); }); @@ -311,7 +337,7 @@ describe("VaultPopupAutofillService", () => { expect(result).toBe(true); expect(mockCipher.login.uris).toHaveLength(1); expect(mockCipher.login.uris[0].uri).toBe(mockCurrentTab.url); - expect(mockCipherService.encrypt).toHaveBeenCalledWith(mockCipher); + expect(mockCipherService.encrypt).toHaveBeenCalledWith(mockCipher, mockUserId); expect(mockCipherService.updateWithServer).toHaveBeenCalledWith(mockEncryptedCipher); }); diff --git a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts index ca59ffd9979..a2e032a54f1 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts @@ -1,5 +1,7 @@ import { Injectable } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; import { + combineLatest, firstValueFrom, map, Observable, @@ -10,6 +12,7 @@ import { switchMap, } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.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"; @@ -26,20 +29,30 @@ import { } from "../../../autofill/services/abstractions/autofill.service"; import { BrowserApi } from "../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; +import { closeViewVaultItemPopout, VaultPopoutType } from "../utils/vault-popout-window"; @Injectable({ providedIn: "root", }) export class VaultPopupAutofillService { private _refreshCurrentTab$ = new Subject(); - + private senderTabId$: Observable = this.route.queryParams.pipe( + map((params) => (params?.senderTabId ? parseInt(params.senderTabId, 10) : undefined)), + ); /** - * Observable that contains the current tab to be considered for autofill. If there is no current tab - * or the popup is in a popout window, this will be null. + * Observable that contains the current tab to be considered for autofill. + * This can be the tab from the current window if opened in a Popup OR + * the sending tab when opened the single action Popout (specified by the senderTabId route query parameter) */ - currentAutofillTab$: Observable = this._refreshCurrentTab$.pipe( - startWith(null), - switchMap(async () => { + currentAutofillTab$: Observable = combineLatest([ + this.senderTabId$, + this._refreshCurrentTab$.pipe(startWith(null)), + ]).pipe( + switchMap(async ([senderTabId]) => { + if (senderTabId) { + return await BrowserApi.getTab(senderTabId); + } + if (BrowserPopupUtils.inPopout(window)) { return null; } @@ -72,6 +85,8 @@ export class VaultPopupAutofillService { private passwordRepromptService: PasswordRepromptService, private cipherService: CipherService, private messagingService: MessagingService, + private route: ActivatedRoute, + private accountService: AccountService, ) { this._currentPageDetails$.subscribe(); } @@ -122,7 +137,21 @@ export class VaultPopupAutofillService { return true; } - private _closePopup() { + private async _closePopup(cipher: CipherView, tab: chrome.tabs.Tab | null) { + if (BrowserPopupUtils.inSingleActionPopout(window, VaultPopoutType.viewVaultItem) && tab.id) { + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("autoFillSuccess"), + }); + setTimeout(async () => { + await BrowserApi.focusTab(tab.id); + await closeViewVaultItemPopout(`${VaultPopoutType.viewVaultItem}_${cipher.id}`); + }, 1000); + + return; + } + if (!BrowserPopupUtils.inPopup(window)) { return; } @@ -156,7 +185,7 @@ export class VaultPopupAutofillService { const didAutofill = await this._internalDoAutofill(cipher, tab, pageDetails); if (didAutofill && closePopup) { - this._closePopup(); + await this._closePopup(cipher, tab); } return didAutofill; @@ -191,7 +220,7 @@ export class VaultPopupAutofillService { } if (closePopup) { - this._closePopup(); + await this._closePopup(cipher, tab); } else { this.toastService.showToast({ variant: "success", @@ -221,7 +250,10 @@ export class VaultPopupAutofillService { cipher.login.uris.push(loginUri); try { - const encCipher = await this.cipherService.encrypt(cipher); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const encCipher = await this.cipherService.encrypt(cipher, activeUserId); await this.cipherService.updateWithServer(encCipher); this.messagingService.send("editedCipher"); return true; diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts index e9abe7bff26..77a86dd9e88 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts @@ -1,11 +1,12 @@ import { TestBed } from "@angular/core/testing"; import { mock } from "jest-mock-extended"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, firstValueFrom, timeout } from "rxjs"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { ObservableTracker } from "@bitwarden/common/spec"; import { CipherId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -29,6 +30,7 @@ describe("VaultPopupItemsService", () => { let mockOrg: Organization; let mockCollections: CollectionView[]; + let activeUserLastSync$: BehaviorSubject; const cipherServiceMock = mock(); const vaultSettingsServiceMock = mock(); @@ -37,6 +39,7 @@ describe("VaultPopupItemsService", () => { const searchService = mock(); const collectionService = mock(); const vaultAutofillServiceMock = mock(); + const syncServiceMock = mock(); beforeEach(() => { allCiphers = cipherFactory(10); @@ -90,6 +93,9 @@ describe("VaultPopupItemsService", () => { organizationServiceMock.organizations$ = new BehaviorSubject([mockOrg]); collectionService.decryptedCollections$ = new BehaviorSubject(mockCollections); + activeUserLastSync$ = new BehaviorSubject(new Date()); + syncServiceMock.activeUserLastSync$.mockReturnValue(activeUserLastSync$); + testBed = TestBed.configureTestingModule({ providers: [ { provide: CipherService, useValue: cipherServiceMock }, @@ -99,6 +105,7 @@ describe("VaultPopupItemsService", () => { { provide: VaultPopupListFiltersService, useValue: vaultPopupListFiltersServiceMock }, { provide: CollectionService, useValue: collectionService }, { provide: VaultPopupAutofillService, useValue: vaultAutofillServiceMock }, + { provide: SyncService, useValue: syncServiceMock }, ], }); @@ -155,6 +162,14 @@ describe("VaultPopupItemsService", () => { await expect(tracker.pauseUntilReceived(3)).rejects.toThrow("Timeout exceeded"); }); + it("should not emit cipher list if syncService.getLastSync returns null", async () => { + activeUserLastSync$.next(null); + + const obs$ = service.autoFillCiphers$.pipe(timeout(50)); + + await expect(firstValueFrom(obs$)).rejects.toThrow("Timeout has occurred"); + }); + describe("autoFillCiphers$", () => { it("should return empty array if there is no current tab", (done) => { (vaultAutofillServiceMock.currentAutofillTab$ as BehaviorSubject).next(null); @@ -311,6 +326,19 @@ describe("VaultPopupItemsService", () => { done(); }); }); + + it("should return true when all ciphers are deleted", (done) => { + cipherServiceMock.getAllDecrypted.mockResolvedValue([ + { id: "1", type: CipherType.Login, name: "Login 1", isDeleted: true }, + { id: "2", type: CipherType.Login, name: "Login 2", isDeleted: true }, + { id: "3", type: CipherType.Login, name: "Login 3", isDeleted: true }, + ] as CipherView[]); + + service.emptyVault$.subscribe((empty) => { + expect(empty).toBe(true); + done(); + }); + }); }); describe("noFilteredResults$", () => { @@ -330,6 +358,24 @@ describe("VaultPopupItemsService", () => { }); }); + describe("deletedCiphers$", () => { + it("should return deleted ciphers", (done) => { + const ciphers = [ + { id: "1", type: CipherType.Login, name: "Login 1", isDeleted: true }, + { id: "2", type: CipherType.Login, name: "Login 2", isDeleted: true }, + { id: "3", type: CipherType.Login, name: "Login 3", isDeleted: true }, + { id: "4", type: CipherType.Login, name: "Login 4", isDeleted: false }, + ] as CipherView[]; + + cipherServiceMock.getAllDecrypted.mockResolvedValue(ciphers); + + service.deletedCiphers$.subscribe((deletedCiphers) => { + expect(deletedCiphers.length).toBe(3); + done(); + }); + }); + }); + describe("hasFilterApplied$", () => { it("should return true if the search term provided is searchable", (done) => { searchService.isSearchable.mockImplementation(async () => true); @@ -381,19 +427,6 @@ describe("VaultPopupItemsService", () => { expect(tracked.emissions[1]).toBe(true); expect(tracked.emissions[2]).toBe(false); }); - - it("should cycle when filters are applied", async () => { - // Restart tracking - tracked = new ObservableTracker(service.loading$); - service.applyFilter("test"); - - await trackedCiphers.pauseUntilReceived(2); - - expect(tracked.emissions.length).toBe(3); - expect(tracked.emissions[0]).toBe(false); - expect(tracked.emissions[1]).toBe(true); - expect(tracked.emissions[2]).toBe(false); - }); }); describe("applyFilter", () => { diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index 1c26f9cc712..3714d07241f 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -5,9 +5,11 @@ import { concatMap, distinctUntilChanged, distinctUntilKeyChanged, + filter, from, map, merge, + MonoTypeOperatorFunction, Observable, of, shareReplay, @@ -21,6 +23,7 @@ import { import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SyncService } from "@bitwarden/common/platform/sync"; import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; @@ -29,6 +32,7 @@ import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { runInsideAngular } from "../../../platform/browser/run-inside-angular.operator"; +import { waitUntil } from "../../util"; import { PopupCipherView } from "../views/popup-cipher.view"; import { VaultPopupAutofillService } from "./vault-popup-autofill.service"; @@ -72,13 +76,18 @@ export class VaultPopupItemsService { * Observable that contains the list of all decrypted ciphers. * @private */ - private _cipherList$: Observable = merge( + private _allDecryptedCiphers$: Observable = merge( this.cipherService.ciphers$, this.cipherService.localData$, ).pipe( runInsideAngular(inject(NgZone)), // Workaround to ensure cipher$ state provider emissions are run inside Angular tap(() => this._ciphersLoading$.next()), + waitUntilSync(this.syncService), switchMap(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted())), + shareReplay({ refCount: true, bufferSize: 1 }), + ); + + private _activeCipherList$: Observable = this._allDecryptedCiphers$.pipe( switchMap((ciphers) => combineLatest([ this.organizationService.organizations$, @@ -87,26 +96,26 @@ export class VaultPopupItemsService { map(([organizations, collections]) => { const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org])); const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col])); - return ciphers.map( - (cipher) => - new PopupCipherView( - cipher, - cipher.collectionIds?.map((colId) => collectionMap[colId as CollectionId]), - orgMap[cipher.organizationId as OrganizationId], - ), - ); + return ciphers + .filter((c) => !c.isDeleted) + .map( + (cipher) => + new PopupCipherView( + cipher, + cipher.collectionIds?.map((colId) => collectionMap[colId as CollectionId]), + orgMap[cipher.organizationId as OrganizationId], + ), + ); }), ), ), - shareReplay({ refCount: true, bufferSize: 1 }), ); private _filteredCipherList$: Observable = combineLatest([ - this._cipherList$, + this._activeCipherList$, this._searchText$, this.vaultPopupListFiltersService.filterFunction$, ]).pipe( - tap(() => this._ciphersLoading$.next()), map(([ciphers, searchText, filterFunction]): [CipherView[], string] => [ filterFunction(ciphers), searchText, @@ -202,7 +211,9 @@ export class VaultPopupItemsService { /** * Observable that indicates whether the user's vault is empty. */ - emptyVault$: Observable = this._cipherList$.pipe(map((ciphers) => !ciphers.length)); + emptyVault$: Observable = this._activeCipherList$.pipe( + map((ciphers) => !ciphers.length), + ); /** * Observable that indicates whether there are no ciphers to show with the current filter. @@ -226,6 +237,14 @@ export class VaultPopupItemsService { }), ); + /** + * Observable that contains the list of ciphers that have been deleted. + */ + deletedCiphers$: Observable = this._allDecryptedCiphers$.pipe( + map((ciphers) => ciphers.filter((c) => c.isDeleted)), + shareReplay({ refCount: false, bufferSize: 1 }), + ); + constructor( private cipherService: CipherService, private vaultSettingsService: VaultSettingsService, @@ -234,6 +253,7 @@ export class VaultPopupItemsService { private searchService: SearchService, private collectionService: CollectionService, private vaultPopupAutofillService: VaultPopupAutofillService, + private syncService: SyncService, ) {} applyFilter(newSearchText: string) { @@ -264,3 +284,11 @@ export class VaultPopupItemsService { return this.cipherService.sortCiphersByLastUsedThenName(a, b); } } + +/** + * Operator that waits until the active account has synced at least once before allowing the source to continue emission. + * @param syncService + */ +const waitUntilSync = (syncService: SyncService): MonoTypeOperatorFunction => { + return waitUntil(syncService.activeUserLastSync$().pipe(filter((lastSync) => lastSync != null))); +}; diff --git a/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts b/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts new file mode 100644 index 00000000000..151f8517d57 --- /dev/null +++ b/apps/browser/src/vault/popup/services/vault-ui-onboarding.service.ts @@ -0,0 +1,88 @@ +import { Injectable } from "@angular/core"; +import { firstValueFrom, map } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { + GlobalState, + KeyDefinition, + StateProvider, + VAULT_BROWSER_UI_ONBOARDING, +} from "@bitwarden/common/platform/state"; +import { DialogService } from "@bitwarden/components"; + +import { VaultUiOnboardingComponent } from "../components/vault-v2/vault-ui-onboarding/vault-ui-onboarding.component"; + +// Key definition for the Vault UI onboarding state. +// This key is used to store the state of the new UI information dialog. +export const GLOBAL_VAULT_UI_ONBOARDING = new KeyDefinition( + VAULT_BROWSER_UI_ONBOARDING, + "dialogState", + { + deserializer: (obj) => obj, + }, +); + +@Injectable() +export class VaultUiOnboardingService { + // TODO: Update this date to the release date of the new Browser UI + private onboardingUiReleaseDate = new Date("2024-07-25"); + + private vaultUiOnboardingState: GlobalState = this.stateProvider.getGlobal( + GLOBAL_VAULT_UI_ONBOARDING, + ); + + private readonly vaultUiOnboardingState$ = this.vaultUiOnboardingState.state$.pipe( + map((x) => x ?? false), + ); + + constructor( + private stateProvider: StateProvider, + private dialogService: DialogService, + private apiService: ApiService, + ) {} + + /** + * Checks whether the onboarding dialog should be shown and opens it if necessary. + * The dialog is shown if the user has not previously viewed it and is not a new account. + */ + async showOnboardingDialog(): Promise { + const hasViewedDialog = await this.getVaultUiOnboardingState(); + + if (!hasViewedDialog && !(await this.isNewAccount())) { + await this.openVaultUiOnboardingDialog(); + } + } + + private async openVaultUiOnboardingDialog(): Promise { + const dialogRef = VaultUiOnboardingComponent.open(this.dialogService); + + const result = firstValueFrom(dialogRef.closed); + + // Update the onboarding state when the dialog is closed + await this.setVaultUiOnboardingState(true); + + return result; + } + + private async isNewAccount(): Promise { + const userProfile = await this.apiService.getProfile(); + const profileCreationDate = new Date(userProfile.creationDate); + return profileCreationDate > this.onboardingUiReleaseDate; + } + + /** + * Updates and saves the state indicating whether the user has viewed + * the new UI onboarding information dialog. + */ + private async setVaultUiOnboardingState(value: boolean): Promise { + await this.vaultUiOnboardingState.update(() => value); + } + + /** + * Retrieves the current state indicating whether the user has viewed + * the new UI onboarding information dialog.s + */ + private async getVaultUiOnboardingState(): Promise { + return await firstValueFrom(this.vaultUiOnboardingState$); + } +} 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/appearance.component.html b/apps/browser/src/vault/popup/settings/appearance.component.html index 36b21905127..a431fc72a1f 100644 --- a/apps/browser/src/vault/popup/settings/appearance.component.html +++ b/apps/browser/src/vault/popup/settings/appearance.component.html @@ -64,4 +64,17 @@ {{ accountSwitcherEnabled ? ("faviconDescAlt" | i18n) : ("faviconDesc" | i18n) }}
+
+
+
+ + +
+
+
diff --git a/apps/browser/src/vault/popup/settings/appearance.component.ts b/apps/browser/src/vault/popup/settings/appearance.component.ts index 154d4e426d8..1095b56a75c 100644 --- a/apps/browser/src/vault/popup/settings/appearance.component.ts +++ b/apps/browser/src/vault/popup/settings/appearance.component.ts @@ -3,6 +3,7 @@ import { firstValueFrom } from "rxjs"; import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; +import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.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"; @@ -20,6 +21,7 @@ export class AppearanceComponent implements OnInit { theme: ThemeType; themeOptions: any[]; accountSwitcherEnabled = false; + enableRoutingAnimation: boolean; constructor( private messagingService: MessagingService, @@ -27,6 +29,7 @@ export class AppearanceComponent implements OnInit { private badgeSettingsService: BadgeSettingsServiceAbstraction, i18nService: I18nService, private themeStateService: ThemeStateService, + private animationControlService: AnimationControlService, ) { this.themeOptions = [ { name: i18nService.t("default"), value: ThemeType.System }, @@ -40,6 +43,10 @@ export class AppearanceComponent implements OnInit { } async ngOnInit() { + this.enableRoutingAnimation = await firstValueFrom( + this.animationControlService.enableRoutingAnimation$, + ); + this.enableFavicon = await firstValueFrom(this.domainSettingsService.showFavicons$); this.enableBadgeCounter = await firstValueFrom(this.badgeSettingsService.enableBadgeCounter$); @@ -47,6 +54,10 @@ export class AppearanceComponent implements OnInit { this.theme = await firstValueFrom(this.themeStateService.selectedTheme$); } + async updateRoutingAnimation() { + await this.animationControlService.setEnableRoutingAnimation(this.enableRoutingAnimation); + } + async updateFavicon() { await this.domainSettingsService.setShowFavicons(this.enableFavicon); } diff --git a/apps/browser/src/vault/popup/settings/folder-add-edit.component.ts b/apps/browser/src/vault/popup/settings/folder-add-edit.component.ts index ca24de56ef9..b873735b460 100644 --- a/apps/browser/src/vault/popup/settings/folder-add-edit.component.ts +++ b/apps/browser/src/vault/popup/settings/folder-add-edit.component.ts @@ -1,4 +1,4 @@ -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { first } from "rxjs/operators"; @@ -16,7 +16,7 @@ import { DialogService } from "@bitwarden/components"; templateUrl: "folder-add-edit.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class FolderAddEditComponent extends BaseFolderAddEditComponent { +export class FolderAddEditComponent extends BaseFolderAddEditComponent implements OnInit { constructor( folderService: FolderService, folderApiService: FolderApiServiceAbstraction, 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/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.html b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.html new file mode 100644 index 00000000000..2ccfeaf3459 --- /dev/null +++ b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.html @@ -0,0 +1,40 @@ + + +

+ {{ headerText }} +

+ {{ ciphers.length }} +
+ + + + + {{ cipher.name }} + + + + + + + + + + + + +
diff --git a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts new file mode 100644 index 00000000000..6d3bfc24838 --- /dev/null +++ b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts @@ -0,0 +1,113 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; +import { Router } from "@angular/router"; + +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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + DialogService, + IconButtonModule, + ItemModule, + MenuModule, + SectionComponent, + SectionHeaderComponent, + ToastService, + TypographyModule, +} from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +@Component({ + selector: "app-trash-list-items-container", + templateUrl: "trash-list-items-container.component.html", + standalone: true, + imports: [ + CommonModule, + ItemModule, + JslibModule, + SectionComponent, + SectionHeaderComponent, + MenuModule, + IconButtonModule, + TypographyModule, + ], +}) +export class TrashListItemsContainerComponent { + /** + * The list of trashed items to display. + */ + @Input() + ciphers: CipherView[] = []; + + @Input() + headerText: string; + + constructor( + private cipherService: CipherService, + private logService: LogService, + private toastService: ToastService, + private i18nService: I18nService, + private dialogService: DialogService, + private passwordRepromptService: PasswordRepromptService, + private router: Router, + ) {} + + async restore(cipher: CipherView) { + try { + await this.cipherService.restoreWithServer(cipher.id); + + await this.router.navigate(["/vault"]); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("restoredItem"), + }); + } catch (e) { + this.logService.error(e); + } + } + + async delete(cipher: CipherView) { + const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher); + + if (!repromptPassed) { + return; + } + + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "deleteItem" }, + content: { key: "permanentlyDeleteItemConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + await this.cipherService.deleteWithServer(cipher.id); + + await this.router.navigate(["/vault"]); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("permanentlyDeletedItem"), + }); + } catch (e) { + this.logService.error(e); + } + } + + async onViewCipher(cipher: CipherView) { + const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher); + if (!repromptPassed) { + return; + } + + await this.router.navigate(["/view-cipher"], { + queryParams: { cipherId: cipher.id, type: cipher.type }, + }); + } +} diff --git a/apps/browser/src/vault/popup/settings/trash.component.html b/apps/browser/src/vault/popup/settings/trash.component.html new file mode 100644 index 00000000000..146e4161671 --- /dev/null +++ b/apps/browser/src/vault/popup/settings/trash.component.html @@ -0,0 +1,37 @@ + + + + + + + + + {{ "trashWarning" | i18n }} + + + + + + + + + + {{ "noItemsInTrash" | i18n }} + + + {{ "noItemsInTrashDesc" | i18n }} + + + + + diff --git a/apps/browser/src/vault/popup/settings/trash.component.ts b/apps/browser/src/vault/popup/settings/trash.component.ts new file mode 100644 index 00000000000..b6f77ef6a52 --- /dev/null +++ b/apps/browser/src/vault/popup/settings/trash.component.ts @@ -0,0 +1,37 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { CalloutModule, NoItemsModule } from "@bitwarden/components"; +import { VaultIcons } from "@bitwarden/vault"; + +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 { VaultListItemsContainerComponent } from "../components/vault-v2/vault-list-items-container/vault-list-items-container.component"; +import { VaultPopupItemsService } from "../services/vault-popup-items.service"; + +import { TrashListItemsContainerComponent } from "./trash-list-items-container/trash-list-items-container.component"; + +@Component({ + templateUrl: "trash.component.html", + standalone: true, + imports: [ + CommonModule, + JslibModule, + PopupPageComponent, + PopupHeaderComponent, + PopOutComponent, + VaultListItemsContainerComponent, + TrashListItemsContainerComponent, + CalloutModule, + NoItemsModule, + ], +}) +export class TrashComponent { + protected deletedCiphers$ = this.vaultPopupItemsService.deletedCiphers$; + + protected emptyTrashIcon = VaultIcons.EmptyTrash; + + constructor(private vaultPopupItemsService: VaultPopupItemsService) {} +} diff --git a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html index 10243bdaa9f..03dd1182fbb 100644 --- a/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html +++ b/apps/browser/src/vault/popup/settings/vault-settings-v2.component.html @@ -24,6 +24,12 @@ + + + {{ "trash" | i18n }} + + + @@ -99,7 +99,7 @@ class="btn block" type="button" routerLink="/accessibility-cookie" - (click)="setLoginEmailValues()" + (click)="saveEmailSettings()" > {{ "loadAccessibilityCookie" | i18n }} @@ -139,7 +139,7 @@ type="button" class="text text-primary password-hint-btn" routerLink="/hint" - (click)="setLoginEmailValues()" + (click)="saveEmailSettings()" > {{ "getMasterPasswordHint" | i18n }} diff --git a/apps/desktop/src/auth/login/login.component.ts b/apps/desktop/src/auth/login/login.component.ts index d37e9961b2f..2b5910baa96 100644 --- a/apps/desktop/src/auth/login/login.component.ts +++ b/apps/desktop/src/auth/login/login.component.ts @@ -1,4 +1,4 @@ -import { Component, NgZone, OnDestroy, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, NgZone, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { Subject, takeUntil } from "rxjs"; @@ -9,13 +9,13 @@ import { ModalService } from "@bitwarden/angular/services/modal.service"; import { LoginStrategyServiceAbstraction, LoginEmailServiceAbstraction, + RegisterRouteService, } from "@bitwarden/auth/common"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -23,7 +23,9 @@ 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 { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { EnvironmentComponent } from "../environment.component"; @@ -34,7 +36,7 @@ const BroadcasterSubscriptionId = "LoginComponent"; selector: "app-login", templateUrl: "login.component.html", }) -export class LoginComponent extends BaseLoginComponent implements OnDestroy { +export class LoginComponent extends BaseLoginComponent implements OnInit, OnDestroy { @ViewChild("environment", { read: ViewContainerRef, static: true }) environmentModal: ViewContainerRef; @@ -72,7 +74,8 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { loginEmailService: LoginEmailServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction, - configService: ConfigService, + registerRouteService: RegisterRouteService, + toastService: ToastService, ) { super( devicesApiService, @@ -93,7 +96,8 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { loginEmailService, ssoLoginService, webAuthnLoginService, - configService, + registerRouteService, + toastService, ); super.onSuccessfulLogin = () => { return syncService.fullSync(true); @@ -161,11 +165,11 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { async continue() { await super.validateEmail(); if (!this.formGroup.controls.email.valid) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccured"), - this.i18nService.t("invalidEmail"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccured"), + message: this.i18nService.t("invalidEmail"), + }); return; } this.focusInput(); @@ -187,4 +191,40 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { const email = this.loggedEmail; document.getElementById(email == null || email === "" ? "email" : "masterPassword")?.focus(); } + + async launchSsoBrowser(clientId: string, ssoRedirectUri: string) { + if (!ipc.platform.isAppImage && !ipc.platform.isSnapStore && !ipc.platform.isDev) { + return super.launchSsoBrowser(clientId, ssoRedirectUri); + } + + // Save off email for SSO + await this.ssoLoginService.setSsoEmail(this.formGroup.value.email); + + // Generate necessary sso params + const passwordOptions: any = { + type: "password", + length: 64, + uppercase: true, + lowercase: true, + numbers: true, + special: false, + }; + const state = await this.passwordGenerationService.generatePassword(passwordOptions); + const ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions); + const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256"); + const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash); + + // Save sso params + await this.ssoLoginService.setSsoState(state); + await this.ssoLoginService.setCodeVerifier(ssoCodeVerifier); + try { + await ipc.platform.localhostCallbackService.openSsoPrompt(codeChallenge, state); + } catch (err) { + this.platformUtilsService.showToast( + "error", + this.i18nService.t("errorOccured"), + this.i18nService.t("ssoError"), + ); + } + } } diff --git a/apps/desktop/src/auth/register.component.ts b/apps/desktop/src/auth/register.component.ts index be44c276485..e7c2cfd32b3 100644 --- a/apps/desktop/src/auth/register.component.ts +++ b/apps/desktop/src/auth/register.component.ts @@ -14,7 +14,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; const BroadcasterSubscriptionId = "RegisterComponent"; @@ -41,6 +41,7 @@ export class RegisterComponent extends BaseRegisterComponent implements OnInit, logService: LogService, auditService: AuditService, dialogService: DialogService, + toastService: ToastService, ) { super( formValidationErrorService, @@ -57,6 +58,7 @@ export class RegisterComponent extends BaseRegisterComponent implements OnInit, logService, auditService, dialogService, + toastService, ); } diff --git a/apps/desktop/src/auth/set-password.component.ts b/apps/desktop/src/auth/set-password.component.ts index 3012f646036..21bc7e8db14 100644 --- a/apps/desktop/src/auth/set-password.component.ts +++ b/apps/desktop/src/auth/set-password.component.ts @@ -1,11 +1,11 @@ -import { Component, NgZone, OnDestroy } from "@angular/core"; +import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; +import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component"; import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; @@ -14,6 +14,7 @@ import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -21,7 +22,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; const BroadcasterSubscriptionId = "SetPasswordComponent"; @@ -30,7 +31,7 @@ const BroadcasterSubscriptionId = "SetPasswordComponent"; selector: "app-set-password", templateUrl: "set-password.component.html", }) -export class SetPasswordComponent extends BaseSetPasswordComponent implements OnDestroy { +export class SetPasswordComponent extends BaseSetPasswordComponent implements OnInit, OnDestroy { constructor( accountService: AccountService, masterPasswordService: InternalMasterPasswordServiceAbstraction, @@ -49,11 +50,13 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On private ngZone: NgZone, stateService: StateService, organizationApiService: OrganizationApiServiceAbstraction, - organizationUserService: OrganizationUserService, + organizationUserApiService: OrganizationUserApiService, userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, dialogService: DialogService, kdfConfigService: KdfConfigService, + encryptService: EncryptService, + toastService: ToastService, ) { super( accountService, @@ -71,11 +74,13 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On route, stateService, organizationApiService, - organizationUserService, + organizationUserApiService, userDecryptionOptionsService, ssoLoginService, dialogService, kdfConfigService, + encryptService, + toastService, ); } diff --git a/apps/desktop/src/auth/sso.component.ts b/apps/desktop/src/auth/sso.component.ts index 234ebc85cee..6821a548945 100644 --- a/apps/desktop/src/auth/sso.component.ts +++ b/apps/desktop/src/auth/sso.component.ts @@ -18,6 +18,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @Component({ @@ -43,6 +44,7 @@ export class SsoComponent extends BaseSsoComponent { configService: ConfigService, masterPasswordService: InternalMasterPasswordServiceAbstraction, accountService: AccountService, + toastService: ToastService, ) { super( ssoLoginService, @@ -61,6 +63,7 @@ export class SsoComponent extends BaseSsoComponent { configService, masterPasswordService, accountService, + toastService, ); super.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/desktop/src/auth/two-factor-auth-duo.component.ts b/apps/desktop/src/auth/two-factor-auth-duo.component.ts new file mode 100644 index 00000000000..e22fd3ee612 --- /dev/null +++ b/apps/desktop/src/auth/two-factor-auth-duo.component.ts @@ -0,0 +1,113 @@ +import { DialogModule } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; +import { ReactiveFormsModule, FormsModule } from "@angular/forms"; +import { firstValueFrom } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; +import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { + AsyncActionsModule, + ButtonModule, + FormFieldModule, + LinkModule, + ToastService, + TypographyModule, +} from "@bitwarden/components"; + +import { TwoFactorAuthDuoComponent as TwoFactorAuthDuoBaseComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-duo.component"; + +const BroadcasterSubscriptionId = "TwoFactorComponent"; + +@Component({ + standalone: true, + selector: "app-two-factor-auth-duo", + templateUrl: + "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-duo.component.html", + imports: [ + CommonModule, + JslibModule, + DialogModule, + ButtonModule, + LinkModule, + TypographyModule, + ReactiveFormsModule, + FormFieldModule, + AsyncActionsModule, + FormsModule, + ], + providers: [I18nPipe], +}) +export class TwoFactorAuthDuoComponent + extends TwoFactorAuthDuoBaseComponent + implements OnInit, OnDestroy +{ + constructor( + protected i18nService: I18nService, + protected platformUtilsService: PlatformUtilsService, + private broadcasterService: BroadcasterService, + private ngZone: NgZone, + private environmentService: EnvironmentService, + toastService: ToastService, + ) { + super(i18nService, platformUtilsService, toastService); + } + + async ngOnInit(): Promise { + await super.ngOnInit(); + } + + duoCallbackSubscriptionEnabled: boolean = false; + + protected override setupDuoResultListener() { + if (!this.duoCallbackSubscriptionEnabled) { + this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { + await this.ngZone.run(async () => { + if (message.command === "duoCallback") { + this.token.emit(message.code + "|" + message.state); + } + }); + }); + this.duoCallbackSubscriptionEnabled = true; + } + } + + override async launchDuoFrameless() { + if (this.duoFramelessUrl === null) { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("duoHealthCheckResultsInNullAuthUrlError"), + }); + return; + } + const duoHandOffMessage = { + title: this.i18nService.t("youSuccessfullyLoggedIn"), + message: this.i18nService.t("youMayCloseThisWindow"), + isCountdown: false, + }; + + // we're using the connector here as a way to set a cookie with translations + // before continuing to the duo frameless url + const env = await firstValueFrom(this.environmentService.environment$); + const launchUrl = + env.getWebVaultUrl() + + "/duo-redirect-connector.html" + + "?duoFramelessUrl=" + + encodeURIComponent(this.duoFramelessUrl) + + "&handOffMessage=" + + encodeURIComponent(JSON.stringify(duoHandOffMessage)); + this.platformUtilsService.launchUri(launchUrl); + } + + async ngOnDestroy() { + if (this.duoCallbackSubscriptionEnabled) { + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + this.duoCallbackSubscriptionEnabled = false; + } + } +} diff --git a/apps/desktop/src/auth/two-factor-auth.component.ts b/apps/desktop/src/auth/two-factor-auth.component.ts new file mode 100644 index 00000000000..29271b565c1 --- /dev/null +++ b/apps/desktop/src/auth/two-factor-auth.component.ts @@ -0,0 +1,50 @@ +import { DialogModule } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { ReactiveFormsModule } from "@angular/forms"; +import { RouterLink } from "@angular/router"; + +import { TwoFactorAuthAuthenticatorComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-authenticator.component"; +import { TwoFactorAuthEmailComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-email.component"; +import { TwoFactorAuthWebAuthnComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-webauthn.component"; +import { TwoFactorAuthYubikeyComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-yubikey.component"; +import { TwoFactorAuthComponent as BaseTwoFactorAuthComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component"; +import { TwoFactorOptionsComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-options.component"; +import { JslibModule } from "../../../../libs/angular/src/jslib.module"; +import { AsyncActionsModule } from "../../../../libs/components/src/async-actions"; +import { ButtonModule } from "../../../../libs/components/src/button"; +import { CheckboxModule } from "../../../../libs/components/src/checkbox"; +import { FormFieldModule } from "../../../../libs/components/src/form-field"; +import { LinkModule } from "../../../../libs/components/src/link"; +import { I18nPipe } from "../../../../libs/components/src/shared/i18n.pipe"; +import { TypographyModule } from "../../../../libs/components/src/typography"; + +import { TwoFactorAuthDuoComponent } from "./two-factor-auth-duo.component"; + +@Component({ + standalone: true, + templateUrl: + "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html", + selector: "app-two-factor-auth", + imports: [ + CommonModule, + JslibModule, + DialogModule, + ButtonModule, + LinkModule, + TypographyModule, + ReactiveFormsModule, + FormFieldModule, + AsyncActionsModule, + RouterLink, + CheckboxModule, + TwoFactorOptionsComponent, + TwoFactorAuthEmailComponent, + TwoFactorAuthAuthenticatorComponent, + TwoFactorAuthYubikeyComponent, + TwoFactorAuthDuoComponent, + TwoFactorAuthWebAuthnComponent, + ], + providers: [I18nPipe], +}) +export class TwoFactorAuthComponent extends BaseTwoFactorAuthComponent {} diff --git a/apps/desktop/src/auth/two-factor.component.ts b/apps/desktop/src/auth/two-factor.component.ts index d1b84c1fa0e..d2c5efe929f 100644 --- a/apps/desktop/src/auth/two-factor.component.ts +++ b/apps/desktop/src/auth/two-factor.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject, NgZone, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, Inject, NgZone, OnDestroy, ViewChild, ViewContainerRef } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; @@ -25,6 +25,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { TwoFactorOptionsComponent } from "./two-factor-options.component"; @@ -35,7 +36,7 @@ const BroadcasterSubscriptionId = "TwoFactorComponent"; templateUrl: "two-factor.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class TwoFactorComponent extends BaseTwoFactorComponent { +export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDestroy { @ViewChild("twoFactorOptions", { read: ViewContainerRef, static: true }) twoFactorOptionsModal: ViewContainerRef; @@ -64,6 +65,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { configService: ConfigService, masterPasswordService: InternalMasterPasswordServiceAbstraction, accountService: AccountService, + toastService: ToastService, @Inject(WINDOW) protected win: Window, ) { super( @@ -85,6 +87,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { configService, masterPasswordService, accountService, + toastService, ); super.onSuccessfulLogin = async () => { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -149,6 +152,15 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { } override async launchDuoFrameless() { + if (this.duoFramelessUrl === null) { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("duoHealthCheckResultsInNullAuthUrlError"), + }); + return; + } + const duoHandOffMessage = { title: this.i18nService.t("youSuccessfullyLoggedIn"), message: this.i18nService.t("youMayCloseThisWindow"), diff --git a/apps/desktop/src/images/two-factor/0.png b/apps/desktop/src/images/two-factor/0.png index f37e3f17b4d..307ff4fd60f 100644 Binary files a/apps/desktop/src/images/two-factor/0.png and b/apps/desktop/src/images/two-factor/0.png differ diff --git a/apps/desktop/src/images/two-factor/1-w.png b/apps/desktop/src/images/two-factor/1-w.png new file mode 100644 index 00000000000..a4e39b3f466 Binary files /dev/null and b/apps/desktop/src/images/two-factor/1-w.png differ diff --git a/apps/desktop/src/images/two-factor/1.png b/apps/desktop/src/images/two-factor/1.png index b47a12b1db8..37fb7bc4327 100644 Binary files a/apps/desktop/src/images/two-factor/1.png and b/apps/desktop/src/images/two-factor/1.png differ diff --git a/apps/desktop/src/images/two-factor/2.png b/apps/desktop/src/images/two-factor/2.png index ab2e4340361..d069bdab992 100644 Binary files a/apps/desktop/src/images/two-factor/2.png and b/apps/desktop/src/images/two-factor/2.png differ diff --git a/apps/desktop/src/images/two-factor/3.png b/apps/desktop/src/images/two-factor/3.png index 21aac2da678..c543343f53b 100644 Binary files a/apps/desktop/src/images/two-factor/3.png and b/apps/desktop/src/images/two-factor/3.png differ diff --git a/apps/desktop/src/images/two-factor/4.png b/apps/desktop/src/images/two-factor/4.png index ae7d7b55e49..058671ea37e 100644 Binary files a/apps/desktop/src/images/two-factor/4.png and b/apps/desktop/src/images/two-factor/4.png differ diff --git a/apps/desktop/src/images/two-factor/6.png b/apps/desktop/src/images/two-factor/6.png index ab2e4340361..d069bdab992 100644 Binary files a/apps/desktop/src/images/two-factor/6.png and b/apps/desktop/src/images/two-factor/6.png differ diff --git a/apps/desktop/src/images/two-factor/7-w.png b/apps/desktop/src/images/two-factor/7-w.png new file mode 100644 index 00000000000..89fdd8a2a08 Binary files /dev/null and b/apps/desktop/src/images/two-factor/7-w.png differ diff --git a/apps/desktop/src/images/two-factor/7.png b/apps/desktop/src/images/two-factor/7.png new file mode 100644 index 00000000000..2a38bdcd3e5 Binary files /dev/null and b/apps/desktop/src/images/two-factor/7.png differ diff --git a/apps/desktop/src/images/two-factor/rc-w.png b/apps/desktop/src/images/two-factor/rc-w.png new file mode 100644 index 00000000000..e83b8db1324 Binary files /dev/null and b/apps/desktop/src/images/two-factor/rc-w.png differ diff --git a/apps/desktop/src/images/two-factor/rc.png b/apps/desktop/src/images/two-factor/rc.png new file mode 100644 index 00000000000..4bebdf936cc Binary files /dev/null and b/apps/desktop/src/images/two-factor/rc.png differ diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index 3e643925e7a..4db80761659 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Instellings" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "U nuwe rekening is geskep! U kan nou aanteken." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "Ons het ’n e-pos gestuur met u hoofwagwoordwenk." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Bevestigingskode word vereis." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Ongeldige bevestigingskode" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "U aantekensessie het verstryk." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Is u seker u wil uitteken?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "U kan lidmaatskap op die bitwarden.com-webkluis koop. Wil u nou na die webwerf gaan?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "U is ’n premie-lid!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "U nuwe hoofwagwoord voldoen nie aan die beleidsvereistes nie." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "Vir blaaierbiometrie moet werkskermbiometrie eers in instellings geaktiveer wees." }, + "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": "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." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Vereis e-posbevestiging" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "U moet u e-pos bevestig om die funksie te gebruik." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "U kluisuittelling oorskry die beperkinge wat deur u organisasie daargestel is." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Outomatiese inskrywing" }, @@ -2524,7 +2593,7 @@ "message": "You have been logged out because your access token could not be decrypted. Please log in again to resolve this issue." }, "refreshTokenSecureStorageRetrievalFailure": { - "message": "You have been logged out because your refresh token could not be retrieved. Please log in again to resolve this issue." + "message": "Jy was uitgeteken omdat jou verfris teken nie opgespoor kon word nie. Teken asseblief weer in op die kwesie optelos." }, "masterPasswordHint": { "message": "Jou hoofwagwoord kan nie herkry word as jy dit vergeet nie!" @@ -2545,10 +2614,10 @@ "message": "Aanbevole bywerking van instellings" }, "deviceApprovalRequired": { - "message": "Device approval required. Select an approval option below:" + "message": "Toestel goedkeuring word vereis. Kies 'n goedkeuings opsie hieronder:" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Onthou hierdie toestel" }, "uncheckIfPublicDevice": { "message": "Uncheck if using a public device" @@ -2560,10 +2629,10 @@ "message": "Request admin approval" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "Keur goed met hoofwagwoord" }, "region": { - "message": "Region" + "message": "Streek" }, "ssoIdentifierRequired": { "message": "Organization SSO identifier is required." @@ -2573,10 +2642,10 @@ "description": "European Union" }, "loggingInOn": { - "message": "Logging in on" + "message": "Meld aan op" }, "selfHostedServer": { - "message": "self-hosted" + "message": "self-gehuisves" }, "accessDenied": { "message": "Toegang geweier. U het nie toestemming om hierdie blad te sien nie." @@ -2597,22 +2666,22 @@ "message": "Trouble logging in?" }, "loginApproved": { - "message": "Login approved" + "message": "Aanmelding goedgekeur" }, "userEmailMissing": { - "message": "User email missing" + "message": "Gebruiker-e-pos is vermis" }, "deviceTrusted": { - "message": "Device trusted" + "message": "Toestel is vertroud" }, "inputRequired": { - "message": "Input is required." + "message": "Inset word vereis." }, "required": { - "message": "required" + "message": "vereiste" }, "search": { - "message": "Search" + "message": "Soek" }, "inputMinLength": { "message": "Input must be at least $COUNT$ characters long.", @@ -2724,11 +2793,11 @@ "message": "Alias domain" }, "importData": { - "message": "Import data", + "message": "Invoer data", "description": "Used for the desktop menu item and the header of the import dialog" }, "importError": { - "message": "Import error" + "message": "Invoer fout" }, "importErrorDesc": { "message": "There was a problem with the data you tried to import. Please resolve the errors listed below in your source file and try again." @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2797,7 +2869,7 @@ "message": "Select a collection" }, "importTargetHint": { - "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", + "message": "Kies dié opsie as jy die inhoud van die ingevoerde lêer na $DESTINATION$ wil skuif", "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", "placeholders": { "destination": { @@ -2807,25 +2879,25 @@ } }, "importUnassignedItemsError": { - "message": "File contains unassigned items." + "message": "Lêer bevat ontoegewysde items." }, "selectFormat": { - "message": "Select the format of the import file" + "message": "Kies die formaat van die invoer lêer" }, "selectImportFile": { - "message": "Select the import file" + "message": "Kies die invoer lêer" }, "chooseFile": { - "message": "Choose File" + "message": "Kies Lêer" }, "noFileChosen": { - "message": "No file chosen" + "message": "Geen Lêer gekies" }, "orCopyPasteFileContents": { - "message": "or copy/paste the import file contents" + "message": "of kopieer/plak die invoer lêer inhoud" }, "instructionsFor": { - "message": "$NAME$ Instructions", + "message": "$NAME$ Instruksies", "description": "The title for the import tool instructions.", "placeholders": { "name": { @@ -2835,89 +2907,89 @@ } }, "confirmVaultImport": { - "message": "Confirm vault import" + "message": "Bevesting kluis invoer" }, "confirmVaultImportDesc": { - "message": "This file is password-protected. Please enter the file password to import data." + "message": "Dié lêer is wagwoord-beskermed. Voer asseblief dié lêer se wagwoord in om data in te voer." }, "confirmFilePassword": { - "message": "Confirm file password" + "message": "Bevesting lêer-wagwoord" }, "exportSuccess": { - "message": "Vault data exported" + "message": "Kluis data uitgevoer" }, "multifactorAuthenticationCancelled": { - "message": "Multifactor authentication cancelled" + "message": "Multifaktor waarmerking gekanselleer" }, "noLastPassDataFound": { - "message": "No LastPass data found" + "message": "Geen LastPass data gevind" }, "incorrectUsernameOrPassword": { - "message": "Incorrect username or password" + "message": "Verkeerde gerbuikersnaam of wagwoord" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "Verkeerde wagwoord" }, "incorrectCode": { - "message": "Incorrect code" + "message": "Verkeerde kode" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "Verkeerde PIN" }, "multifactorAuthenticationFailed": { - "message": "Multifactor authentication failed" + "message": "Multifaktor waarmerking gemisluk" }, "includeSharedFolders": { - "message": "Include shared folders" + "message": "Sluit gedeelde lêers in" }, "lastPassEmail": { - "message": "LastPass Email" + "message": "LastPass E-pos" }, "importingYourAccount": { - "message": "Importing your account..." + "message": "Jou rekening word ingevoer..." }, "lastPassMFARequired": { - "message": "LastPass multifactor authentication required" + "message": "LastPass multifaktor waarmerking word vereis" }, "lastPassMFADesc": { - "message": "Enter your one-time passcode from your authentication app" + "message": "Voer jou eenmalige wagkode van jou waarmerkings-toep" }, "lastPassOOBDesc": { "message": "Approve the login request in your authentication app or enter a one-time passcode." }, "passcode": { - "message": "Passcode" + "message": "Wagkode" }, "lastPassMasterPassword": { - "message": "LastPass master password" + "message": "LastPass hoofwagwoord" }, "lastPassAuthRequired": { - "message": "LastPass authentication required" + "message": "LastPass waarmerking word vereis" }, "awaitingSSO": { - "message": "Awaiting SSO authentication" + "message": "Wag SSO waarmerking af" }, "awaitingSSODesc": { - "message": "Please continue to log in using your company credentials." + "message": "Asseblief gaan voort om aantemeld met jou maatskappy se magtigingsbewyse." }, "seeDetailedInstructions": { - "message": "See detailed instructions on our help site at", + "message": "Sien gedetaleerde instruksies op ons help werf by", "description": "This is followed a by a hyperlink to the help website." }, "importDirectlyFromLastPass": { - "message": "Import directly from LastPass" + "message": "Voer in direk van LastPass" }, "importFromCSV": { - "message": "Import from CSV" + "message": "Voer van CSV in" }, "lastPassTryAgainCheckEmail": { - "message": "Try again or look for an email from LastPass to verify it's you." + "message": "Probeer weer of kyk vir 'n e-pos van LastPass om te verifieer dat dit jy is." }, "collection": { - "message": "Collection" + "message": "Versameling" }, "lastPassYubikeyDesc": { - "message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button." + "message": "Plass die YubiKey wat met jou LastPass-rekening geassosieer word in jou rekenaar se USB-poort in en raak dan aan die knoppie." }, "commonImportFormats": { "message": "Common formats", @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "Lêer stuur" + }, + "textSends": { + "message": "Teks stuur" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index b80ec1b4dc9..abbd203b505 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -500,10 +500,10 @@ "message": "إنشاء حساب" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "تعيين كلمة مرور قوية" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" + "message": "إنهاء إنشاء حسابك عن طريق تعيين كلمة مرور" }, "logIn": { "message": "تسجيل الدخول" @@ -527,7 +527,7 @@ "message": "تلميح كلمة المرور الرئيسية (اختياري)" }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "إذا نسيت كلمة المرور الخاصة بك، يمكن إرسال تلميح كلمة المرور إلى بريدك الإلكتروني. $CURRENT$/$MAXIMUM$ الحد الأقصى للحرف.", "placeholders": { "current": { "content": "$1", @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "كلمة المرور الرئيسية" + }, + "masterPassImportant": { + "message": "لا يمكن استعادة كلمة المرور الرئيسية إذا نسيتها!" + }, + "confirmMasterPassword": { + "message": "تأكيد كلمة المرور الرئيسية" + }, + "masterPassHintLabel": { + "message": "تلميح كلمة المرور الرئيسية" + }, + "joinOrganization": { + "message": "الانضمام إلى المؤسسة" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "إنهاء الانضمام إلى هذه المؤسسة عن طريق تعيين كلمة مرور رئيسية." + }, "settings": { "message": "الإعدادات" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "تم إنشاء حسابك الجديد! يمكنك الآن تسجيل الدخول." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "لقد أرسلنا لك رسالة بريد إلكتروني تحتوي على تلميحات كلمة المرور الرئيسية." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "رمز التحقق مطلوب." }, + "webauthnCancelOrTimeout": { + "message": "تم إلغاء المصادقة أو استغرقت وقتا طويلا. الرجاء المحاولة مرة أخرى." + }, "invalidVerificationCode": { "message": "رمز التحقق غير صالح" }, @@ -667,17 +694,17 @@ "message": "تطبيق المصادقة" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "أدخل رمز تم إنشاؤه بواسطة تطبيق مصادقة مثل Bitwarden Authenticer.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP security key" + "message": "مفتاح أمان YubiKey OTP" }, "yubiKeyDesc": { "message": "استخدم YubiKey للوصول إلى حسابك. يعمل مع YubiKey 4 ،4 Nano ،4C، وأجهزة NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "أدخل الرمز الذي تم إنشاؤه بواسطة نظام حماية Duo.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -694,7 +721,7 @@ "message": "البريد الإلكتروني" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "أدخل رمزا أرسل إلى بريدك الإلكتروني." }, "loginUnavailable": { "message": "تسجيل الدخول غير متاح" @@ -777,6 +804,18 @@ "loginExpired": { "message": "انتهت صلاحية جلسة الدخول." }, + "restartRegistration": { + "message": "إعادة تشغيل التسجيل" + }, + "expiredLink": { + "message": "رابط منتهي الصلاحية" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "الرجاء إعادة تشغيل التسجيل أو محاولة تسجيل الدخول." + }, + "youMayAlreadyHaveAnAccount": { + "message": "قد يكون لديك حساب بالفعل" + }, "logOutConfirmation": { "message": "هل أنت متأكد من أنك تريد تسجيل الخروج؟" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "يمكنك شراء العضوية المتميزة على bitwarden.com على خزانة الويب. هل تريد زيارة الموقع الآن؟" }, + "premiumPurchaseAlertV2": { + "message": "يمكنك شراء Premium من إعدادات حسابك على تطبيق Bitwarden على شبكة الإنترنت." + }, "premiumCurrentMember": { "message": "أنت عضو مميز!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "خطأ في تحديث رمز الوصول" }, @@ -1477,9 +1522,15 @@ "additionalWindowsHelloSettings": { "message": "إعدادات Windows Hello إضافية" }, + "unlockWithPolkit": { + "message": "فتح مع مصادقة النظام" + }, "windowsHelloConsentMessage": { "message": "تحقق من Bitwarden." }, + "polkitConsentMessage": { + "message": "مصادقة لفتح Bitwarden." + }, "unlockWithTouchId": { "message": "فتح بواسطة معرف اللمس" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "اسأل عن Windows Hello عند التشغيل" }, + "autoPromptPolkit": { + "message": "طلب مصادقة النظام عند التشغيل" + }, "autoPromptTouchId": { "message": "اطلب معرف اللمس عند التشغيل" }, @@ -1678,20 +1732,20 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "كلمة المرور الرئيسية الجديدة لا تفي بمتطلبات السياسة العامة." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "احصل على المشورة والإعلانات وفرص البحث من Bitwarden في صندوق الوارد الخاص بك." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "إلغاء الاشتراك" }, "atAnyTime": { - "message": "at any time." + "message": "في أي وقت." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "من خلال المتابعة، أنت توافق على" }, "and": { - "message": "and" + "message": "و" }, "acceptPolicies": { "message": "من خلال تحديد هذا المربع فإنك توافق على ما يلي:" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "القياسات الحيوية للمتصفح تتطلب القياسات الحيوية لسطح المكتب ليتم تمكينها في الإعدادات أولاً." }, + "biometricsManualSetupTitle": { + "message": "الإعداد التلقائي غير متوفر" + }, + "biometricsManualSetupDesc": { + "message": "بسبب طريقة التثبيت، لا يمكن تمكين دعم القياسات الحيوية تلقائياً. هل ترغب في فتح الوثائق حول كيفية القيام بذلك يدوياً؟" + }, "personalOwnershipSubmitError": { "message": "بسبب سياسة المؤسسة، يمنع عليك حفظ العناصر في خزانتك الشخصية. غيّر خيار الملكية إلى مؤسسة واختر من المجموعات المتاحة." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "التحقق من البريد الإلكتروني مطلوب" }, + "emailVerifiedV2": { + "message": "تم التحقق من البريد الإلكتروني" + }, "emailVerificationRequiredDesc": { "message": "يجب عليك التحقق من بريدك الإلكتروني لاستخدام هذه الميزة." }, @@ -1979,6 +2042,9 @@ "updateWeakMasterPasswordWarning": { "message": "كلمة المرور الرئيسية الخاصة بك لا تفي بواحدة أو أكثر من سياسات مؤسستك. من أجل الوصول إلى الخزنة، يجب عليك تحديث كلمة المرور الرئيسية الآن. سيتم تسجيل خروجك من الجلسة الحالية، مما يتطلب منك تسجيل الدخول مرة أخرى. وقد تظل الجلسات النشطة على أجهزة أخرى نشطة لمدة تصل إلى ساعة واحدة." }, + "tdeDisabledMasterPasswordRequired": { + "message": "لقد قامت مؤسستك بتعطيل تشفير الجهاز الموثوق به. الرجاء تعيين كلمة مرور رئيسية للوصول إلى خزانك." + }, "tryAgain": { "message": "حاول مرة أخرى" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "مهلة خزنتك تتجاوز القيود التي تضعها مؤسستك." }, + "inviteAccepted": { + "message": "تم قبول الدعوة" + }, "resetPasswordPolicyAutoEnroll": { "message": "التسجيل التلقائي" }, @@ -2706,7 +2775,7 @@ "message": "قائمة فرعية" }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "تبديل التنقل الجانبي" }, "skipToContent": { "message": "تخطي إلى المحتوى" @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "خطأ في الاتصال بخدمة Duo. استخدم طريقة تسجيل الدخول بخطوتين مختلفة أو اتصل بـ Duo للمساعدة." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "قم بتشغيل دوو واتبع الخطوات لإنهاء تسجيل الدخول." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "كلمة مرور الملف غير صالحة، الرجاء استخدام كلمة المرور التي أدخلتها عند إنشاء ملف التصدير." }, - "importDestination": { - "message": "وجهة الاستيراد" + "destination": { + "message": "الوجهة" }, "learnAboutImportOptions": { "message": "تعرف على خيارات الاستيراد الخاصة بك" @@ -2844,7 +2916,7 @@ "message": "تأكيد كلمة مرور الملف" }, "exportSuccess": { - "message": "Vault data exported" + "message": "تم تصدير بيانات المخزن" }, "multifactorAuthenticationCancelled": { "message": "تم إلغاء المصادقة المتعددة" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "البيانات" + }, + "fileSends": { + "message": "إرسال الملف" + }, + "textSends": { + "message": "إرسال النص" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 3481c03dfaa..3ab79fad39a 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Ana parol" + }, + "masterPassImportant": { + "message": "Unutsanız, ana parolunuz geri qaytarıla bilməz!" + }, + "confirmMasterPassword": { + "message": "Ana parolu təsdiqlə" + }, + "masterPassHintLabel": { + "message": "Ana parol ipucusu" + }, + "joinOrganization": { + "message": "Təşkilata qoşul" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Bu ana parol təyin edərək bu təşkilata qoşulmağı tamamlayın." + }, "settings": { "message": "Ayarlar" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Yeni hesabınız yaradıldı! İndi giriş edə bilərsiniz." }, + "newAccountCreated2": { + "message": "Yeni hesabınız yaradıldı!" + }, + "youHaveBeenLoggedIn": { + "message": "Giriş etdiniz!" + }, "masterPassSent": { "message": "Ana parol məsləhətini ehtiva edən bir e-poçt göndərdik." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Doğrulama kodu tələb olunur." }, + "webauthnCancelOrTimeout": { + "message": "Kimlik doğrulama ləğv edildi və ya çox uzun çəkdi. Lütfən yenidən sınayın." + }, "invalidVerificationCode": { "message": "Yararsız doğrulama kodu" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Seansın müddəti bitdi." }, + "restartRegistration": { + "message": "Qeydiyyatı yenidən başlat" + }, + "expiredLink": { + "message": "Vaxtı bitmiş keçid" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Lütfən qeydiyyatı yenidən başladın və ya giriş etməyə çalışın." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Artıq bir hesabınız ola bilər" + }, "logOutConfirmation": { "message": "Çıxış etmək istədiyinizə əminsiniz?" }, @@ -826,7 +865,7 @@ "message": "Bizi izləyin" }, "syncVault": { - "message": "Anbarı eyniləşdir" + "message": "Anbarı sinxronlaşdır" }, "changeMasterPass": { "message": "Ana parolu dəyişdir" @@ -855,10 +894,10 @@ "message": "Brauzer uzantısını al" }, "syncingComplete": { - "message": "Eyniləşdirmə tamamlandı" + "message": "Sinxr tamamlandı" }, "syncingFailed": { - "message": "Uğursuz eyniləşdirmə" + "message": "Sinxr uğursuz oldu" }, "yourVaultIsLocked": { "message": "Anbarınız kilidlənib. Davam etmək üçün kimliyinizi doğrulayın." @@ -889,10 +928,10 @@ "message": "İki mərhələli giriş" }, "vaultTimeout": { - "message": "Anbara müraciət bitəcək" + "message": "Anbar vaxtının bitməsi" }, "vaultTimeoutDesc": { - "message": "Anbara müraciətin bitəcəyi vaxtı seçin və seçilən əməliyyatı icra edin." + "message": "Anbarın vaxt bitmə əməliyyatını nə vaxt icra edəcəyini seçin." }, "immediately": { "message": "Dərhal" @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Premium üzvlüyü bitwarden.com veb anbarında satın ala bilərsiniz. İndi saytı ziyarət etmək istəyirsiniz?" }, + "premiumPurchaseAlertV2": { + "message": "Bitwarden veb tətbiqindəki hesab ayarlarınızda Premium satın ala bilərsiniz." + }, "premiumCurrentMember": { "message": "Premium üzvsünüz!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Müraciət tokeni təzələmə xətası" }, @@ -1384,7 +1429,7 @@ "description": "ex. Register as an accessibility user at hcaptcha.com" }, "copyPasteLink": { - "message": "E-poçtunuza göndərilən bağlantını kopyalayıb aşağıda yapışdırın" + "message": "E-poçtunuza göndərilən keçidi kopyalayıb aşağıda yapışdırın" }, "enterhCaptchaUrl": { "message": "hCaptcha əlçatımlılıq çərəzini yükləmək üçün ünvanı daxil edin", @@ -1477,9 +1522,15 @@ "additionalWindowsHelloSettings": { "message": "Əlavə Windows Hello ayarları" }, + "unlockWithPolkit": { + "message": "Sistem kimlik doğrulaması ilə kilidi aç" + }, "windowsHelloConsentMessage": { "message": "Bitwarden üçün doğrula." }, + "polkitConsentMessage": { + "message": "Bitwarden kilidini açmaq üçün kimliyi doğrula." + }, "unlockWithTouchId": { "message": "Touch ID kilidini aç" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "Açılışda Windows Hello-nu soruşun" }, + "autoPromptPolkit": { + "message": "Açılışda sistem kimlik doğrulamasını tələb et" + }, "autoPromptTouchId": { "message": "Açılışda Touch ID-ni soruşun" }, @@ -1566,13 +1620,13 @@ "message": "Bir və ya daha çox təşkilat siyasətləri yaradıcı seçimlərinizə təsir edir." }, "vaultTimeoutAction": { - "message": "Anbara müraciət vaxtının bitmə əməliyyatı" + "message": "Anbar vaxtının bitmə əməliyyatı" }, "vaultTimeoutActionLockDesc": { - "message": "Kilidli bir anbar, təkrar müraciət etmək üçün ana parolunuzu yenidən yazmağınızı tələb edir." + "message": "Anbarınıza təkrar müraciət etmək üçün ana parol və ya digər kilid açma üsulu tələb olunur." }, "vaultTimeoutActionLogOutDesc": { - "message": "Anbarınıza yenidən müraciət etmək üçün təkrar kimlik doğrulama tələb olunur." + "message": "Anbarınıza təkrar müraciət etmək üçün təkrar kimlik doğrulama tələb olunur." }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Anbar vaxt bitməsi əməliyyatınızı dəyişdirmək üçün bir kilid açma üsulu quraşdırın." @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Yeni ana parolunuz siyasət tələblərini qarşılamır." }, - "receiveMarketingEmails": { - "message": "Elanlar, məsləhətlər və araşdırma fürsətləri üçün Bitwarden-dən e-poçt alın." + "receiveMarketingEmailsV2": { + "message": "Bitwarden-in tövsiyə, elan və araşdırma imkanlarını gələn qutunuzda əldə edin." }, "unsubscribe": { "message": "Abunəlikdən çıx" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "Brauzer biometrikləri, əvvəlcə ayarlarda masaüstü biometriklərinin qurulmasını tələb edir." }, + "biometricsManualSetupTitle": { + "message": "Avtomatik quraşdırma mövcud deyil" + }, + "biometricsManualSetupDesc": { + "message": "Quraşdırma üsuluna görə, biometrik dəstəyi avtomatik fəallaşdırıla bilmədi. Bunun necə manual edildiyi ilə bağlı sənədləşdirmələri açmaq istəyirsiniz?" + }, "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." }, @@ -1845,11 +1905,11 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLink": { - "message": "\"Send\" bağlantısı", + "message": "\"Send\" keçidi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLinkLabel": { - "message": "\"Send\" bağlantısı", + "message": "\"Send\" keçidi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "textHiddenByDefault": { @@ -1905,11 +1965,11 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "copySendLinkToClipboard": { - "message": "Send bağlantısını lövhəyə kopyala", + "message": "Send keçidini lövhəyə kopyala", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "copySendLinkOnSave": { - "message": "Saxladıqdan sonra bu \"Send\"in paylaşma bağlantısını lövhəmə kopyala." + "message": "Saxladıqdan sonra bu \"Send\"in paylaşma keçidini lövhəmə kopyala." }, "sendDisabled": { "message": "Send sıradan çıxarıldı", @@ -1920,7 +1980,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "copyLink": { - "message": "Bağlantını kopyala" + "message": "Keçidi kopyala" }, "disabled": { "message": "Sıradan çıxarıldı" @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "E-poçtun doğrulanması tələb olunur" }, + "emailVerifiedV2": { + "message": "E-poçt doğrulandı" + }, "emailVerificationRequiredDesc": { "message": "Bu özəlliyi istifadə etmək üçün e-poçtunuzu doğrulamalısınız." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Anbar vaxt bitişi, təşkilatınız tərəfindən ayarlanan məhdudiyyətləri aşır." }, + "inviteAccepted": { + "message": "Dəvət qəbul edildi" + }, "resetPasswordPolicyAutoEnroll": { "message": "Avtomatik qeydiyyat" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Duo xidmətinə bağlanarkən xəta baş verdi. Fərqli iki addımlı giriş üsulu istifadə edin və ya kömək üçün Duo ilə əlaqə saxlayın." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Duo-nu başladın və giriş prosesini tamamlamaq üçün addımları izləyin." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Yararsız fayl parolu, lütfən xaricə köçürmə faylını yaradarkən daxil etdiyiniz parolu istifadə edin." }, - "importDestination": { - "message": "Hədəfi daxilə köçür" + "destination": { + "message": "Hədəf" }, "learnAboutImportOptions": { "message": "Daxilə köçürmə seçimlərinizi öyrənin" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "Fayl \"Send\"ləri" + }, + "textSends": { + "message": "Mətn \"Send\"ləri" + }, + "ssoError": { + "message": "SSO giriş üçün açıq port tapıla bilmədi." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index c1fa1119706..dc270253c03 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Налады" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Ваш уліковы запіс створаны! Цяпер вы можаце ўвайсці ў яго." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "Мы адправілі вам на электронную пошту падказку да асноўнага пароля." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Патрабуецца праверачны код." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Памылковы праверачны код" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Тэрмін дзеяння вашага сеансу завяршыўся." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Вы сапраўды хочаце выйсці?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Вы можаце купіць прэміяльны статус на bitwarden.com. Перайсці на вэб-сайт зараз?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "У вас прэміяльны статус!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,15 @@ "additionalWindowsHelloSettings": { "message": "Дадатковыя налады Windows Hello" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "Праверыць на Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "Разблакіраваць з Touch ID" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "Пытацца пра Windows Hello пры запуску" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Пытацца пра Touch ID пры запуску" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Ваш новы асноўны пароль не адпавядае патрабаванням палітыкі." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "Для актывацыі біяметрыі ў браўзеры неабходна спачатку ўключыць яе ў наладах праграмы для камп'ютара." }, + "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": "У адпаведнасці з палітыкай прадпрыемства вам забаронена захоўваць элементы ў асабістым сховішчы. Змяніце параметры ўласнасці на арганізацыю і выберыце з даступных калекцый." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Патрабуецца праверка электроннай пошты" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Для выкарыстання гэтай функцыі патрабуецца праверыць электронную пошту." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Час чакання вашага сховішча перавышае дазволеныя абмежаванні, якія прызначыла ваша арганізацыя." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Аўтаматычная рэгістрацыя" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 23657992a5e..5ad0e391938 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Главна парола" + }, + "masterPassImportant": { + "message": "Главната парола не може да бъде възстановена, ако я забравите!" + }, + "confirmMasterPassword": { + "message": "Потвърждаване на главната парола" + }, + "masterPassHintLabel": { + "message": "Подсказка за главната парола" + }, + "joinOrganization": { + "message": "Присъединяване към организацията" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Завършете присъединяването си към тази организация като зададете главна парола." + }, "settings": { "message": "Настройки" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Абонаментът ви бе създаден. Вече можете да се впишете." }, + "newAccountCreated2": { + "message": "Новата Ви регистрация беше създадена!" + }, + "youHaveBeenLoggedIn": { + "message": "Вече сте вписан(а)!" + }, "masterPassSent": { "message": "Изпратили сме ви е-писмо с подсказка за главната ви парола." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Кодът за потвърждение е задължителен." }, + "webauthnCancelOrTimeout": { + "message": "Удостоверяването беше отменено или отне твърде много време. Моля, опитайте отново." + }, "invalidVerificationCode": { "message": "Грешен код за потвърждаване" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Сесията ви изтече." }, + "restartRegistration": { + "message": "Рестартиране на регистрацията" + }, + "expiredLink": { + "message": "Връзка с изтекла давност" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Рестартирайте регистрацията или опитайте да се впишете." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Може вече да имате регистрация" + }, "logOutConfirmation": { "message": "Сигурни ли сте, че искате да се отпишете?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Може да платите абонамента си през сайта bitwarden.com. Искате ли да го посетите сега?" }, + "premiumPurchaseAlertV2": { + "message": "Можете да закупите платената версия от настройките на регистрацията си, в приложението по уеб на Битуорден." + }, "premiumCurrentMember": { "message": "Честито, ползвате платен абонамент!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Грешка при опресняването на идентификатора за достъп" }, @@ -1477,9 +1522,15 @@ "additionalWindowsHelloSettings": { "message": "Допълнителни настройки на Windows Hello" }, + "unlockWithPolkit": { + "message": "Отключване чрез системно удостоверяване" + }, "windowsHelloConsentMessage": { "message": "Потвърждаване за Битуорден." }, + "polkitConsentMessage": { + "message": "Идентифицирайте се, за да отключите Битуорден." + }, "unlockWithTouchId": { "message": "Отключване с Touch ID" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "Питане за Windows Hello при пускане" }, + "autoPromptPolkit": { + "message": "Питане за системно удостоверяване при стартиране" + }, "autoPromptTouchId": { "message": "Питане за Touch ID при пускане" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Паролата ви не отговаря на политиките." }, - "receiveMarketingEmails": { - "message": "Получавайте е-писма от Битоурден за новини, съвети и възможности за проучвания." + "receiveMarketingEmailsV2": { + "message": "Получавайте съвети, обявления и предложения за участие в проучвания от Битуорден в пощенската си кутия." }, "unsubscribe": { "message": "Отписване" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "Потвърждаването с биометрични данни в браузъра изисква включването включването им в настройките за самостоятелното приложение." }, + "biometricsManualSetupTitle": { + "message": "Автоматичното настройване не е налично" + }, + "biometricsManualSetupDesc": { + "message": "Поради начина на инсталиране, поддръжката на биометрични данни не може да бъде включена автоматично. Искате ли да отворите документацията, за да видите как да го направите ръчно?" + }, "personalOwnershipSubmitError": { "message": "Заради някоя политика за голяма организация не може да запазвате елементи в собствения си трезор. Променете собствеността да е на организация и изберете от наличните колекции." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Изисква се потвърждение на е-пощата" }, + "emailVerifiedV2": { + "message": "Е-пощата е потвърдена" + }, "emailVerificationRequiredDesc": { "message": "Трябва да потвърдите е-пощата си, за да можете да използвате тази функционалност." }, @@ -1979,6 +2042,9 @@ "updateWeakMasterPasswordWarning": { "message": "Вашата главна парола не отговаря на една или повече политики на организацията Ви. За да получите достъп до трезора, трябва да промените главната си парола сега. Това означава, че ще бъдете отписан(а) от текущата си сесия и ще трябва да се впишете отново. Активните сесии на други устройства може да продължат да бъдат активни още един час." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Вашата организация е деактивирала шифроването чрез доверени устройства. Задайте главна парола, за да получите достъп до трезора си." + }, "tryAgain": { "message": "Нов опит" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Времето за достъп до трезора Ви превишава ограничението, определено от организацията Ви." }, + "inviteAccepted": { + "message": "Поканата е приета" + }, "resetPasswordPolicyAutoEnroll": { "message": "Автоматично включване" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Грешка при свързването с услугата на Duo. Използвайте друг метод за двустепенно удостоверяване или се свържете с Duo за съдействие." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Стартирайте Duo и следвайте инструкциите, за да завършите вписването." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Неправилна парола за файла. Използвайте паролата, която сте въвели при създаването на изнесения файл." }, - "importDestination": { - "message": "Място на внасяне" + "destination": { + "message": "Дестинация" }, "learnAboutImportOptions": { "message": "Научете повече относно възможностите за внасяне" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Данни" + }, + "fileSends": { + "message": "Файлови изпращания" + }, + "textSends": { + "message": "Текстови изпращания" + }, + "ssoError": { + "message": "Не могат да бъдат открити свободни портове за еднократната идентификация." + }, + "fileSavedToDevice": { + "message": "Файлът е запазен на устройството. Можете да го намерите в мястото за сваляния на устройството." } } diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index a383dd0a328..98b61b53b75 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "সেটিংস" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "আপনার নতুন অ্যাকাউন্ট তৈরি করা হয়েছে! আপনি এখন প্রবেশ করতে পারেন।" }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "আমরা আপনাকে আপনার মূল পাসওয়ার্ডের ইঙ্গিতসহ একটি ইমেল প্রেরণ করেছি।" }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "যাচাইকরণ কোড প্রয়োজন।" }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "আপনার লগইন মাত্রকালটির মেয়াদ শেষ হয়ে গেছে।" }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "আপনি লগ আউট করতে চান?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "আপনি bitwarden.com ওয়েব ভল্টে প্রিমিয়াম সদস্যতা কিনতে পারেন। আপনি কি এখনই ওয়েবসাইটটি দেখতে চান?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "আপনি প্রিমিয়াম সদস্য!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "আপনার নতুন মূল পাসওয়ার্ড নীতির প্রয়োজনীয়তা পূরণ করে না।" }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "ব্রাউজার বায়োমেট্রিক্সের জন্য প্রথমে সেটিংসে ডেস্কটপ বায়োমেট্রিক সক্ষম করা প্রয়োজন।" }, + "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": "একটি এন্টারপ্রাইজ নীতির কারণে, আপনি আপনার ব্যক্তিগত ভল্টে বস্তুসমূহ সংরক্ষণ করা থেকে সীমাবদ্ধ। একটি প্রতিষ্ঠানের মালিকানা বিকল্পটি পরিবর্তন করুন এবং উপলভ্য সংগ্রহগুলি থেকে চয়ন করুন।" }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index a3e5c4dc2c4..592117cecd2 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Postavke" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Tvoj novi račun je kreiran! Sada se možeš prijaviti." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "Poslali smo vam e-mail sa podsjetnikom za glavnu lozinku." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Verifikacijski kod je neophodan." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Neispravan verifikacijski kod" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Sesija je istekla." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Da li ste sigurni da želite da se odjavite?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Svojim članstvom možeš upravljati na bitwarden.com web trezoru. Želiš li sada posjetiti web stranicu?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Ti si premium član!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "Biometrija preglednika zahtijeva prethodno omogućenu biometriju u Bitwarden desktop aplikaciji." }, + "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": "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." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 41ab3bba0a4..dbc7fd3a7d3 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", @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Contrasenya mestra" + }, + "masterPassImportant": { + "message": "La contrasenya mestra no es pot recuperar si la oblideu!" + }, + "confirmMasterPassword": { + "message": "Confirma la contrasenya mestra" + }, + "masterPassHintLabel": { + "message": "Pista de la contrasenya mestra" + }, + "joinOrganization": { + "message": "Uneix-te a l'organització" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Acabeu d'unir-vos a aquesta organització establint una contrasenya mestra." + }, "settings": { "message": "Configuració" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "El vostre compte s'ha creat correctament. Ara ja podeu iniciar sessió." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "Hem enviat un correu electrònic amb la vostra contrasenya mestra." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "El codi de verificació és obligatori." }, + "webauthnCancelOrTimeout": { + "message": "L'autenticació s'ha cancel·lat o ha tardat massa. Torna-ho a provar." + }, "invalidVerificationCode": { "message": "Codi de verificació no vàlid" }, @@ -671,7 +698,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." @@ -718,7 +745,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." @@ -777,6 +804,18 @@ "loginExpired": { "message": "La vostra sessió ha caducat." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Enllaç caducat" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Segur que voleu tancar la sessió?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Podeu comprar la vostra subscripció a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Sou un membre premium!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1337,7 +1382,7 @@ "description": "ex. Date this password was updated" }, "exportFrom": { - "message": "Export from" + "message": "Exporta des de" }, "exportVault": { "message": "Exporta caixa forta" @@ -1349,7 +1394,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" @@ -1358,13 +1403,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" @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,20 +1732,20 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "La nova contrasenya principal no compleix els requisits de la política." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "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:" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "La biometria del navegador primer necessita habilitar la biomètrica d’escriptori a la configuració." }, + "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": "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." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Es requereix verificació per correu electrònic" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Heu de verificar el vostre correu electrònic per utilitzar aquesta característica." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "El temps d'espera de la caixa forta supera les restriccions establertes per la vostra organització." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Inscripció automàtica" }, @@ -2482,13 +2551,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." @@ -2497,7 +2566,7 @@ "message": "No email?" }, "goBack": { - "message": "Go back" + "message": "Torna arrere" }, "toEditYourEmailAddress": { "message": "to edit your email address." @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Inicieu DUO i seguiu els passos per finalitzar la sessió." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "La contrasenya del fitxer no és vàlida. Utilitzeu la contrasenya que vau introduir quan vau crear el fitxer d'exportació." }, - "importDestination": { - "message": "Destinació de la importació" + "destination": { + "message": "Destinació" }, "learnAboutImportOptions": { "message": "Obteniu informació sobre les opcions d'importació" @@ -2844,7 +2916,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" @@ -2968,7 +3040,7 @@ } }, "back": { - "message": "Back", + "message": "Arrere", "description": "Button text to navigate back" }, "removeItem": { @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Dades" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index dee3d75f694..05262bc6139 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Hlavní heslo" + }, + "masterPassImportant": { + "message": "Pokud zapomenete Vaše hlavní heslo, nebude možné jej obnovit!" + }, + "confirmMasterPassword": { + "message": "Potvrzení hlavního hesla" + }, + "masterPassHintLabel": { + "message": "Nápověda k hlavnímu heslu" + }, + "joinOrganization": { + "message": "Přidat se k organizaci" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Dokončete připojení k této organizaci nastavením hlavního hesla." + }, "settings": { "message": "Nastavení" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Váš účet byl vytvořen! Můžete se přihlásit." }, + "newAccountCreated2": { + "message": "Váš nový účet byl vytvořen!" + }, + "youHaveBeenLoggedIn": { + "message": "Byli jste přihlášeni!" + }, "masterPassSent": { "message": "Poslali jsme vám e-mail s nápovědou k hlavnímu heslu." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Je vyžadován ověřovací kód." }, + "webauthnCancelOrTimeout": { + "message": "Ověření bylo zrušeno nebo trvalo příliš dlouho. Zkuste to znovu." + }, "invalidVerificationCode": { "message": "Neplatný ověřovací kód" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Platnost přihlášení vypršela." }, + "restartRegistration": { + "message": "Restartovat registraci" + }, + "expiredLink": { + "message": "Platnost odkazu vypršela" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Restartujte registraci nebo se zkuste přihlásit." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Už možná máte účet" + }, "logOutConfirmation": { "message": "Opravdu se chcete odhlásit?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Prémiové členství můžete zakoupit na webové stránce bitwarden.com. Chcete tuto stránku nyní otevřít?" }, + "premiumPurchaseAlertV2": { + "message": "Premium si můžete zakoupit v nastavení účtu ve webové aplikaci Bitwarden." + }, "premiumCurrentMember": { "message": "Jste prémiovým členem!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Kopírování bylo úspěšné" + }, "errorRefreshingAccessToken": { "message": "Chyba aktualizace přístupového tokenu" }, @@ -1477,9 +1522,15 @@ "additionalWindowsHelloSettings": { "message": "Další nastavení Windows Hello" }, + "unlockWithPolkit": { + "message": "Odemknout pomocí systémového ověření" + }, "windowsHelloConsentMessage": { "message": "Ověřte se pro Bitwarden." }, + "polkitConsentMessage": { + "message": "Ověřte se pro odemknutí Bitwardenu." + }, "unlockWithTouchId": { "message": "Odemknout pomocí Touch ID" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "Požádat o Windows Hello při spuštění aplikace" }, + "autoPromptPolkit": { + "message": "Požádat o systémové ověření při spuštění" + }, "autoPromptTouchId": { "message": "Požádat o Touch ID při spuštění aplikace" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Vaše nové hlavní heslo nesplňuje požadavky zásad." }, - "receiveMarketingEmails": { - "message": "Získejte e-maily od Bitwardenu pro oznámení, poradenství a výzkumné příležitosti." + "receiveMarketingEmailsV2": { + "message": "Dostávejte do své e-mailové schránky rady, oznámení a příležitosti k výzkumu od společnosti Bitwarden." }, "unsubscribe": { "message": "Odhlásit odběr" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "Biometrické prvky v prohlížeči vyžadují, aby byla nastavena biometrie nejprve v aplikaci pro počítač." }, + "biometricsManualSetupTitle": { + "message": "Automatické nastavení není k dispozici" + }, + "biometricsManualSetupDesc": { + "message": "Kvůli metodě instalace nelze automaticky povolit podporu biometrických prvků. Chcete otevřít dokumentaci o tom, jak to provést ručně?" + }, "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í." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Je vyžadováno ověření e-mailu" }, + "emailVerifiedV2": { + "message": "E-mail byl ověřen" + }, "emailVerificationRequiredDesc": { "message": "Pro použití této funkce musíte ověřit svůj e-mail." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Časový limit Vašeho trezoru překračuje omezení stanovená Vaší organizací." }, + "inviteAccepted": { + "message": "Pozvánka byla přijata" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatická registrace" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Chyba při připojování ke službě Duo. Použijte jinou dvoufázovou metodu přihlášení nebo kontaktujte Duo o pomoc." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Spusťte DUO a pro dokončení přihlášení postupujte podle kroků." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Neplatné heslo souboru, použijte heslo zadané při vytvoření souboru exportu." }, - "importDestination": { - "message": "Cíl importu" + "destination": { + "message": "Cíl" }, "learnAboutImportOptions": { "message": "Více o volbách importu" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "Sends se soubory" + }, + "textSends": { + "message": "Sends s texty" + }, + "ssoError": { + "message": "Pro přihlášení SSO nebyly nalezeny žádné volné porty." + }, + "fileSavedToDevice": { + "message": "Soubor byl uložen. Můžete jej nalézt ve stažené složce v zařízení." } } diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 94f20c3e300..d752c63e306 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Settings" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Your login session has expired." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a premium member!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,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 organization and choose from available collections." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index bdb296ba717..0e89a376159 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Hovedadgangskode" + }, + "masterPassImportant": { + "message": "Hovedadgangskoden kan ikke gendannes, hvis den glemmes!" + }, + "confirmMasterPassword": { + "message": "Bekræft hovedadgangskode" + }, + "masterPassHintLabel": { + "message": "Hovedadgangskodetip" + }, + "joinOrganization": { + "message": "Bliv medlem af organisation" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Færdiggør tilmeldingen til denne organisation ved at opsætte en hovedadgangskode." + }, "settings": { "message": "Indstillinger" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Den nye konto er oprettet! Der kan nu logges ind." }, + "newAccountCreated2": { + "message": "Din nye konto er oprettet!" + }, + "youHaveBeenLoggedIn": { + "message": "Du er blevet logget ind!" + }, "masterPassSent": { "message": "Der er sendt en e-mail til dig med dit hovedadgangskodetip." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Bekræftelseskode er obligatorisk." }, + "webauthnCancelOrTimeout": { + "message": "Godkendelsen blev afbrudt eller tog for lang tid. Forsøg igen." + }, "invalidVerificationCode": { "message": "Ugyldig bekræftelseskode" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Loginsessionen er udløbet." }, + "restartRegistration": { + "message": "Genstart registrering" + }, + "expiredLink": { + "message": "Udløbet link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Genstart registreringen, eller prøv at logge ind." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Har allerede oprettet en konto?" + }, "logOutConfirmation": { "message": "Sikker på, at du vil logge ud?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Premium-medlemskab kan købes via bitwarden.com web-boksen. Besøg webstedet nu?" }, + "premiumPurchaseAlertV2": { + "message": "Der kan købes Premium fra kontoindstillingerne via Bitwarden web-appen." + }, "premiumCurrentMember": { "message": "Du er Premium-medlem!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Kopieret" + }, "errorRefreshingAccessToken": { "message": "Adgangstoken genopfriskningsfejl" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Din nye hovedadgangskode opfylder ikke politikkravene." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Få råd, bekendtgørelser og forskningsmuligheder fra Bitwarden i indbakken." }, "unsubscribe": { "message": "Afmeld" @@ -1771,6 +1825,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." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "E-mailbekræftelse kræves" }, + "emailVerifiedV2": { + "message": "E-mail bekræftet" + }, "emailVerificationRequiredDesc": { "message": "Du skal bekræfte din mailadresse for at bruge denne funktion." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Din boks-timeout overskrider de organisationsbestemte restriktioner." }, + "inviteAccepted": { + "message": "Invitation accepteret" + }, "resetPasswordPolicyAutoEnroll": { "message": "Auto-indrullering" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Fejl under forbindelsesoprettelsen til Duo-tjenesten. Brug en anden totrins-indlogningsmetode eller kontakt Duo for hjælp." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Start Duo og følg trinnene for at fuldføre indlogningen." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Ugyldig filadgangskode. Brug samme adgangskode som under oprettelsen af eksportfilen." }, - "importDestination": { - "message": "Importdestination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Læs om importmuligheder" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "Fil-Sends" + }, + "textSends": { + "message": "Tekst-Sends" + }, + "ssoError": { + "message": "Ingen ledige porte fundet til SSO-login." + }, + "fileSavedToDevice": { + "message": "Fil gemt på enheden. Håndtér fra enhedens downloads." } } diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index ffd018bcc72..3809a8b5c60 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master-Passwort" + }, + "masterPassImportant": { + "message": "Dein Master-Passwort kann nicht wiederhergestellt werden, wenn du es vergisst!" + }, + "confirmMasterPassword": { + "message": "Master-Passwort bestätigen" + }, + "masterPassHintLabel": { + "message": "Master-Passwort-Hinweis" + }, + "joinOrganization": { + "message": "Organisation beitreten" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Schließe den Beitritt zu dieser Organisation ab, indem du ein Master-Passwort festlegst." + }, "settings": { "message": "Einstellungen" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Dein neues Konto wurde erstellt! Du kannst dich jetzt anmelden." }, + "newAccountCreated2": { + "message": "Dein neues Konto wurde erstellt!" + }, + "youHaveBeenLoggedIn": { + "message": "Du wurdest angemeldet!" + }, "masterPassSent": { "message": "Wir haben dir eine E-Mail mit dem Master-Passwort-Hinweis gesendet." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Verifizierungscode wird benötigt." }, + "webauthnCancelOrTimeout": { + "message": "Die Authentifizierung wurde abgebrochen oder hat zu lange gedauert. Bitte versuche es erneut." + }, "invalidVerificationCode": { "message": "Ungültiger Verifizierungscode" }, @@ -667,17 +694,17 @@ "message": "Authenticator App" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Gib einen Code ein, der von einer Authentifizierungs-App wie dem Bitwarden Authenticator generiert wurde.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP security key" + "message": "Yubico OTP-Sicherheitsschlüssel" }, "yubiKeyDesc": { "message": "Verwende einen YubiKey, um auf dein Konto zuzugreifen. Funktioniert mit den Geräten YubiKey 4, Nano 4, 4C und NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Gib einen von Duo Security generierten Code ein.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -694,7 +721,7 @@ "message": "E-Mail" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Gib einen an deine E-Mail-Adresse gesendeten Code ein." }, "loginUnavailable": { "message": "Anmeldung nicht verfügbar" @@ -777,6 +804,18 @@ "loginExpired": { "message": "Deine Sitzung ist abgelaufen." }, + "restartRegistration": { + "message": "Registrierung neu starten" + }, + "expiredLink": { + "message": "Abgelaufener Link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Bitte starte die Registrierung erneut oder versuche dich anzumelden." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Du hast möglicherweise bereits ein Konto" + }, "logOutConfirmation": { "message": "Bist du sicher, dass du dich abmelden willst?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Du kannst deine Premium-Mitgliedschaft im bitwarden.com Web-Tresor kaufen. Möchtest du die Webseite jetzt besuchen?" }, + "premiumPurchaseAlertV2": { + "message": "Du kannst Premium über deine Kontoeinstellungen in der Bitwarden Web-App kaufen." + }, "premiumCurrentMember": { "message": "Du bist ein Premium-Mitglied!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Zugangs-Token Aktualisierungsfehler" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "Beim Start nach Windows Hello fragen" }, + "autoPromptPolkit": { + "message": "Beim Start nach Systemauthentifizierung fragen" + }, "autoPromptTouchId": { "message": "Beim Start nach Touch ID fragen" }, @@ -1678,20 +1732,20 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Dein neues Masterpasswort entspricht nicht den Anforderungen der Richtlinie." }, - "receiveMarketingEmails": { - "message": "Erhalte E-Mails von Bitwarden für Ankündigungen, Ratschläge und Forschungsmöglichkeiten." + "receiveMarketingEmailsV2": { + "message": "Erhalte Ratschläge, Ankündigungen und Marktforschungsumfragen von Bitwarden in deinem Posteingang." }, "unsubscribe": { "message": "Deabonnieren" }, "atAnyTime": { - "message": "at any time." + "message": "jederzeit." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Indem du fortfährst, stimmst du den" }, "and": { - "message": "and" + "message": "und" }, "acceptPolicies": { "message": "Durch Anwählen dieses Kästchens erklärst du dich mit folgendem einverstanden:" @@ -1771,6 +1825,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." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "E-Mail-Verifizierung erforderlich" }, + "emailVerifiedV2": { + "message": "E-Mail-Adresse verifiziert" + }, "emailVerificationRequiredDesc": { "message": "Du musst deine E-Mail verifizieren, um diese Funktion nutzen zu können." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Dein Tresor-Timeout überschreitet die von deinem Unternehmen festgelegten Beschränkungen." }, + "inviteAccepted": { + "message": "Einladung angenommen" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatische Registrierung" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Fehler beim Verbinden mit dem Duo-Dienst. Verwende eine andere Zwei-Faktor-Authentifizierungsmethode oder kontaktiere Duo für Hilfe." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Starte DUO und folge den Schritten, um die Anmeldung abzuschließen." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Ungültiges Dateipasswort. Bitte verwende das Passwort, das du beim Erstellen der Exportdatei eingegeben hast." }, - "importDestination": { - "message": "Import-Ziel" + "destination": { + "message": "Ziel" }, "learnAboutImportOptions": { "message": "Erfahre mehr über deine Importoptionen" @@ -2844,7 +2916,7 @@ "message": "Dateipasswort bestätigen" }, "exportSuccess": { - "message": "Vault data exported" + "message": "Tresor-Daten exportiert" }, "multifactorAuthenticationCancelled": { "message": "Multifaktor-Authentifizierung abgebrochen" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Daten" + }, + "fileSends": { + "message": "Datei-Sends" + }, + "textSends": { + "message": "Text-Sends" + }, + "ssoError": { + "message": "Es konnten keine freien Ports für die SSO-Anmeldung gefunden werden." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 911a3735e6d..8d2381113a4 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -6,7 +6,7 @@ "message": "Φίλτρα" }, "allItems": { - "message": "Όλα τα στοιχεία" + "message": "Όλα τα αντικείμενα" }, "favorites": { "message": "Αγαπημένα" @@ -24,7 +24,7 @@ "message": "Ταυτότητα" }, "typeSecureNote": { - "message": "Ασφαλής Σημείωση" + "message": "Ασφαλής σημείωση" }, "folders": { "message": "Φάκελοι" @@ -33,10 +33,10 @@ "message": "Συλλογές" }, "searchVault": { - "message": "Αναζήτηση στο vault" + "message": "Αναζήτηση κρύπτης" }, "addItem": { - "message": "Προσθήκη Στοιχείου" + "message": "Προσθήκη αντικειμένου" }, "shared": { "message": "Κοινοποιήθηκε" @@ -67,7 +67,7 @@ "message": "Συνημμένα" }, "viewItem": { - "message": "Προβολή Στοιχείου" + "message": "Προβολή αντικειμένου" }, "name": { "message": "Όνομα" @@ -98,10 +98,10 @@ "message": "Συνθηματικό" }, "editItem": { - "message": "Επεξεργασία Στοιχείου" + "message": "Επεξεργασία αντικειμένου" }, "emailAddress": { - "message": "Διεύθυνση Email" + "message": "Διεύθυνση ηλ. ταχυδρομείου" }, "verificationCodeTotp": { "message": "Κωδικός Επαλήθευσης (TOTP)" @@ -119,24 +119,24 @@ "message": "Εκκίνηση" }, "copyValue": { - "message": "Αντιγραφή Τιμής", + "message": "Αντιγραφή τιμής", "description": "Copy value to clipboard" }, "minimizeOnCopyToClipboard": { "message": "Ελαχιστοποίηση κατά την αντιγραφή στο πρόχειρο" }, "minimizeOnCopyToClipboardDesc": { - "message": "Ελαχιστοποίηση κατά την αντιγραφή των δεδομένων ενός στοιχείου στο πρόχειρο." + "message": "Ελαχιστοποίηση της εφαρμογής κατά την αντιγραφή των δεδομένων ενός αντικειμένου στο πρόχειρο." }, "toggleVisibility": { - "message": "Εναλλαγή Ορατότητας" + "message": "Εναλλαγή ορατότητας" }, "toggleCollapse": { - "message": "Εναλλαγή Σύμπτυξης", + "message": "Εναλλαγή σύμπτυξης", "description": "Toggling an expand/collapse state." }, "cardholderName": { - "message": "Όνομα κατόχου της κάρτας" + "message": "Όνομα κατόχου κάρτας" }, "number": { "message": "Αριθμός" @@ -148,10 +148,10 @@ "message": "Λήξη" }, "securityCode": { - "message": "Κωδικός Ασφαλείας" + "message": "Κωδικός ασφαλείας" }, "identityName": { - "message": "Όνομα Ταυτότητας" + "message": "Όνομα ταυτότητας" }, "company": { "message": "Εταιρεία" @@ -160,10 +160,10 @@ "message": "ΑΜΚΑ" }, "passportNumber": { - "message": "Αριθμός Διαβατηρίου" + "message": "Αριθμός διαβατηρίου" }, "licenseNumber": { - "message": "Αριθμός Άδειας" + "message": "Αριθμός άδειας" }, "email": { "message": "Email" @@ -175,10 +175,10 @@ "message": "Διεύθυνση" }, "premiumRequired": { - "message": "Απαιτείται Έκδοση Premium" + "message": "Απαιτείται Premium" }, "premiumRequiredDesc": { - "message": "Για να χρησιμοποιήσετε αυτή τη λειτουργία, απαιτείται η έκδοση premium." + "message": "Για να χρησιμοποιήσετε αυτή τη λειτουργία, απαιτείται η Premium συνδρομή." }, "errorOccurred": { "message": "Παρουσιάστηκε σφάλμα." @@ -239,7 +239,7 @@ "message": "Κα" }, "mx": { - "message": "Mx στα Ελληνικά" + "message": "Κ" }, "dr": { "message": "Δρ" @@ -257,7 +257,7 @@ "message": "Άλλες" }, "generatePassword": { - "message": "Δημιουργία Κωδικού" + "message": "Γέννηση κωδικού πρόσβασης" }, "type": { "message": "Τύπος" @@ -266,7 +266,7 @@ "message": "Όνομα" }, "middleName": { - "message": "Μεσαίο Όνομα" + "message": "Μεσαίο όνομα" }, "lastName": { "message": "Επώνυμο" @@ -290,7 +290,7 @@ "message": "Περιοχή / Νομός" }, "zipPostalCode": { - "message": "Ταχυδρομικός Κώδικας" + "message": "Ταχυδρομικός κώδικας" }, "country": { "message": "Χώρα" @@ -311,13 +311,13 @@ "message": "Επεξεργασία" }, "authenticatorKeyTotp": { - "message": "Κλειδί επαλήθευσης (TOTP)" + "message": "Κλειδί αυθεντικοποίησης (TOTP)" }, "folder": { "message": "Φάκελος" }, "newCustomField": { - "message": "Νέο Προσαρμοσμένο Πεδίο" + "message": "Νέο προσαρμοσμένο πεδίο" }, "value": { "message": "Τιμή" @@ -349,25 +349,25 @@ "message": "Απαιτείται όνομα." }, "addedItem": { - "message": "Το στοιχείο προστέθηκε" + "message": "Το αντικείμενο προστέθηκε" }, "editedItem": { - "message": "Το στοιχείο αποθηκεύτηκε" + "message": "Το αντικείμενο αποθηκεύτηκε" }, "deleteItem": { - "message": "Διαγραφή Στοιχείου" + "message": "Διαγραφή στοιχείου" }, "deleteFolder": { - "message": "Διαγραφή Φακέλου" + "message": "Διαγραφή φακέλου" }, "deleteAttachment": { - "message": "Διαγραφή Συνημμένου" + "message": "Διαγραφή συνημμένου" }, "deleteItemConfirmation": { - "message": "Είστε βέβαιοι ότι θέλετε να μετακινήσετε αυτό το στοιχείο στον κάδο απορριμμάτων;" + "message": "Σίγουρα θέλετε να το μετακινήσετε στον κάδο;" }, "deletedItem": { - "message": "Το στοιχείο μετακινήθηκε στον κάδο απορριμάτων" + "message": "Το αντικείμενο μετακινήθηκε στον κάδο απορριμάτων" }, "overwritePasswordConfirmation": { "message": "Είστε βέβαιοι ότι θέλετε να αντικαταστήσετε τον τρέχον κωδικό πρόσβασης;" @@ -379,20 +379,20 @@ "message": "Είστε βέβαιοι ότι θέλετε να αντικαταστήσετε το τρέχον όνομα χρήστη;" }, "noneFolder": { - "message": "Χωρίς Φάκελο", + "message": "Χωρίς φάκελο", "description": "This is the folder for uncategorized items" }, "addFolder": { - "message": "Προσθήκη Φακέλου" + "message": "Προσθήκη φακέλου" }, "editFolder": { - "message": "Επεξεργασία Φακέλου" + "message": "Επεξεργασία φακέλου" }, "regeneratePassword": { - "message": "Επαναδημιουργία Κωδικού" + "message": "Επαναδημιουργία κωδικού πρόσβασης" }, "copyPassword": { - "message": "Αντιγραφή Κωδικού" + "message": "Αντιγραφή κωδικού πρόσβασης" }, "copyUri": { "message": "Αντιγραφή URI" @@ -404,7 +404,7 @@ "message": "Μήκος" }, "passwordMinLength": { - "message": "Ελάχιστο μήκος κωδικού" + "message": "Ελάχιστο μήκος κωδικού πρόσβασης" }, "uppercase": { "message": "Κεφαλαία (A-Z)" @@ -416,13 +416,13 @@ "message": "Αριθμοί (0-9)" }, "specialCharacters": { - "message": "Ειδικοί Χαρακτήρες (!@#$%^&*)" + "message": "Ειδικοί χαρακτήρες (!@#$%^&*)" }, "numWords": { - "message": "Αριθμός Λέξεων" + "message": "Αριθμός λέξεων" }, "wordSeparator": { - "message": "Διαχωριστής Λέξεων" + "message": "Διαχωριστής λέξεων" }, "capitalize": { "message": "Κεφαλαία", @@ -435,30 +435,30 @@ "message": "Κλείσιμο" }, "minNumbers": { - "message": "Ελάχιστα Αριθμητικά Ψηφία" + "message": "Ελάχιστα αριθμητικά ψηφία" }, "minSpecial": { "message": "Ελάχιστοι ειδικοί χαρακτήρες", "description": "Minimum Special Characters" }, "ambiguous": { - "message": "Αποφυγή Αμφιλεγόμενων Χαρακτήρων" + "message": "Αποφυγή αμφιλεγόμενων χαρακτήρων" }, "searchCollection": { - "message": "Αναζήτηση στη Συλλογή" + "message": "Αναζήτηση στη συλλογή" }, "searchFolder": { - "message": "Αναζήτηση στον Φάκελο" + "message": "Αναζήτηση στο φάκελο" }, "searchFavorites": { - "message": "Αναζήτηση στα Αγαπημένα" + "message": "Αναζήτηση στα αγαπημένα" }, "searchType": { - "message": "Αναζήτηση σε αυτόν τον τύπο", + "message": "Αναζήτηση τύπου", "description": "Search item type" }, "newAttachment": { - "message": "Προσθήκη Νέου Συνημμένου" + "message": "Προσθήκη νέου συνημμένου" }, "deletedAttachment": { "message": "Το συνημμένο διαγράφηκε" @@ -467,7 +467,7 @@ "message": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το συνημμένο;" }, "attachmentSaved": { - "message": "Το συννημένο αποθηκεύτηκε" + "message": "Το συνημμένο αποθηκεύτηκε" }, "file": { "message": "Αρχείο" @@ -479,7 +479,7 @@ "message": "Το μέγιστο μέγεθος αρχείου είναι 500 MB." }, "encryptionKeyMigrationRequired": { - "message": "Απαιτείται μεταφορά κλειδιού κρυπτογράφησης. Παρακαλούμε συνδεθείτε μέσω του διαδικτυακής κρύπτης για να ενημερώσετε το κλειδί κρυπτογράφησης." + "message": "Απαιτείται μεταφορά κλειδιού κρυπτογράφησης. Παρακαλούμε συνδεθείτε μέσω της διαδικτυακής κρύπτης για να ενημερώσετε το κλειδί κρυπτογράφησης σας." }, "editedFolder": { "message": "Ο φάκελος αποθηκεύτηκε" @@ -488,7 +488,7 @@ "message": "Ο φάκελος προστέθηκε" }, "deleteFolderConfirmation": { - "message": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν τον φάκελο;" + "message": "Σίγουρα θέλετε να διαγράψετε αυτόν τον φάκελο;" }, "deletedFolder": { "message": "Ο φάκελος διαγράφηκε" @@ -497,13 +497,13 @@ "message": "Συνδεθείτε ή δημιουργήστε ένα νέο λογαριασμό για να αποκτήσετε πρόσβαση στο ασφαλές vault σας." }, "createAccount": { - "message": "Δημιουργία Λογαριασμού" + "message": "Δημιουργία λογαριασμού" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Ορίστε έναν ισχυρό κωδικό πρόσβασης" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" + "message": "Ολοκληρώστε τη δημιουργία του λογαριασμού σας ορίζοντας έναν κωδικό πρόσβασης" }, "logIn": { "message": "Είσοδος" @@ -512,7 +512,7 @@ "message": "Υποβολή" }, "masterPass": { - "message": "Κύριος Κωδικός" + "message": "Κύριος κωδικός πρόσβασης" }, "masterPassDesc": { "message": "Ο κύριος κωδικός είναι ο κωδικός που χρησιμοποιείτε για την πρόσβαση στο vault σας. Είναι πολύ σημαντικό να μην ξεχάσετε τον κύριο κωδικό. Δεν υπάρχει τρόπος να ανακτήσετε τον κωδικό σε περίπτωση που τον ξεχάσετε." @@ -521,13 +521,13 @@ "message": "Η υπόδειξη κύριου κωδικού μπορεί να σας βοηθήσει να θυμηθείτε τον κωδικό σας αν τον ξεχάσετε." }, "reTypeMasterPass": { - "message": "Επιβεβαίωση κύριου κωδικού" + "message": "Εισάγετε ξανά τον κύριο κωδικό πρόσβασης" }, "masterPassHint": { - "message": "Υπόδειξη Κύριου Κωδικού (προαιρετικό)" + "message": "Υπόδειξη κύριου κωδικού πρόσβασης (προαιρετικό)" }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Αν ξεχάσετε τον κωδικό πρόσβασής σας, η υπενθύμιση του κωδικού πρόσβασης μπορεί να σταλεί στο ηλ. ταχυδρομείο σας. $CURRENT$/$MAXIMUM$ μέγιστοι χαρακτήρες.", "placeholders": { "current": { "content": "$1", @@ -539,14 +539,32 @@ } } }, + "masterPassword": { + "message": "Κύριος κωδικός πρόσβασης" + }, + "masterPassImportant": { + "message": "Ο κύριος κωδικός πρόσβασής σας δεν μπορεί να ανακτηθεί αν τον ξεχάσετε!" + }, + "confirmMasterPassword": { + "message": "Επιβεβαίωση κύριου κωδικού πρόσβασης" + }, + "masterPassHintLabel": { + "message": "Υπόδειξη κύριου κωδικού πρόσβασης" + }, + "joinOrganization": { + "message": "Συμμετοχή σε οργανισμό" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Ολοκληρώστε τη συμμετοχή σας σε αυτόν τον οργανισμό ορίζοντας έναν κύριο κωδικό πρόσβασης." + }, "settings": { "message": "Ρυθμίσεις" }, "passwordHint": { - "message": "Υπόδειξη Κωδικού" + "message": "Υπόδειξη κωδικού πρόσβασης" }, "enterEmailToGetHint": { - "message": "Εισάγετε τη διεύθυνση email του λογαριασμού σας για να λάβετε την υπόδειξη του κύριου κωδικού." + "message": "Εισάγετε τη διεύθυνση ηλ. ταχυδρομείου του λογαριασμού σας για να λάβετε την υπόδειξη του κύριου κωδικού πρόσβασης." }, "getMasterPasswordHint": { "message": "Λήψη υπόδειξης κύριου κωδικού" @@ -558,13 +576,13 @@ "message": "Μη έγκυρη διεύθυνση e-mail." }, "masterPasswordRequired": { - "message": "Απαιτείται ο κύριος κωδικός." + "message": "Απαιτείται ο κύριος κωδικός πρόσβασης." }, "confirmMasterPasswordRequired": { - "message": "Απαιτείται επιβεβαίωση του κύριου κωδικού." + "message": "Απαιτείται επαναπληκτρολόγηση του κύριου κωδικού πρόσβασης." }, "masterPasswordMinlength": { - "message": "Ο κύριος κωδικός πρέπει να έχει τουλάχιστον $VALUE$ χαρακτήρες μήκος.", + "message": "Ο κύριος κωδικός πρόσβασης πρέπει να έχει τουλάχιστον $VALUE$ χαρακτήρες μήκος.", "description": "The Master Password must be at least a specific number of characters long.", "placeholders": { "value": { @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Ο λογαριασμός σας έχει δημιουργηθεί! Τώρα μπορείτε να συνδεθείτε." }, + "newAccountCreated2": { + "message": "Ο νέος σας λογαριασμός έχει δημιουργηθεί!" + }, + "youHaveBeenLoggedIn": { + "message": "Έχετε συνδεθεί!" + }, "masterPassSent": { "message": "Σας στείλαμε ένα email με την υπόδειξη του κύριου κωδικού." }, @@ -592,7 +616,7 @@ "message": "Παρουσιάστηκε ένα μη αναμενόμενο σφάλμα." }, "itemInformation": { - "message": "Πληροφορίες Στοιχείου" + "message": "Πληροφορίες αντικειμένου" }, "noItemsInList": { "message": "Δεν υπάρχουν στοιχεία στη λίστα." @@ -601,13 +625,13 @@ "message": "Στείλτε έναν κωδικό επαλήθευσης στο email σας" }, "sendCode": { - "message": "Αποστολή Κωδικού" + "message": "Αποστολή κωδικού" }, "codeSent": { - "message": "Ο Κωδικός Στάλθηκε" + "message": "Ο κωδικός στάλθηκε" }, "verificationCode": { - "message": "Κωδικός Επαλήθευσης" + "message": "Κωδικός επαλήθευσης" }, "confirmIdentity": { "message": "Επιβεβαιώστε την ταυτότητα σας για να συνεχίσετε." @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Απαιτείται ο κωδικός επαλήθευσης." }, + "webauthnCancelOrTimeout": { + "message": "Η αυθεντικοποίηση ακυρώθηκε ή διήρκησε πολύ ώρα. Παρακαλώ προσπαθήστε ξανά." + }, "invalidVerificationCode": { "message": "Μη έγκυρος κωδικός επαλήθευσης" }, @@ -646,38 +673,38 @@ "message": "Να με θυμάσαι" }, "sendVerificationCodeEmailAgain": { - "message": "Επανάληψη αποστολής email με κωδικό επαλήθευσης" + "message": "Επανάληψη αποστολής κωδικού επαλήθευσης μέσω ηλ. ταχυδρομείου" }, "useAnotherTwoStepMethod": { "message": "Χρήση άλλης μεθόδου σύνδεσης δύο βημάτων" }, "insertYubiKey": { - "message": "Τοποθετήστε το YubiKey στη θύρα USB του υπολογιστή σας και έπειτα πατήστε το κουμπί του." + "message": "Τοποθετήστε το YubiKey στη θύρα USB του υπολογιστή σας και έπειτα αγγίξτε το κουμπί του." }, "insertU2f": { "message": "Εισάγετε το κλειδί ασφαλείας στη θύρα USB του υπολογιστή σας. Αν έχει κουμπί, πατήστε το." }, "recoveryCodeDesc": { - "message": "Έχετε χάσει την πρόσβαση σε όλους τους παρόχους δύο παραγόντων; Χρησιμοποιήστε τον κωδικό ανάκτησης για να απενεργοποιήσετε όλους τους παρόχους δύο παραγόντων από το λογαριασμό σας." + "message": "Έχετε χάσει την πρόσβαση σε όλους τους παρόχους δύο παραγόντων; Χρησιμοποιήστε τον κωδικό ανάκτησης σας για να απενεργοποιήσετε όλους τους παρόχους δύο παραγόντων από τον λογαριασμό σας." }, "recoveryCodeTitle": { - "message": "Κωδικός Ανάκτησης" + "message": "Κωδικός ανάκτησης" }, "authenticatorAppTitle": { - "message": "Εφαρμογή Επαλήθευσης" + "message": "Εφαρμογή αυθεντικοποίησης" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Εισάγετε έναν κωδικό που δημιουργήθηκε από μια εφαρμογή αυθεντικοποίησης όπως το Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP security key" + "message": "Κλειδί ασφαλείας YubiKey OTP" }, "yubiKeyDesc": { "message": "Χρησιμοποιήστε ένα YubiKey για να αποκτήσετε πρόσβαση στο λογαριασμό σας. Λειτουργεί με συσκευές σειράς YubiKey 4, 4 Nano, 4C και συσκευές NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Εισάγετε έναν κωδικό που δημιουργήθηκε από το Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -688,19 +715,19 @@ "message": "FIDO2 WebAuthn" }, "webAuthnDesc": { - "message": "Χρησιμοποιήστε οποιοδήποτε κλειδί ασφαλείας WebAuthn για να αποκτήσετε πρόσβαση στο λογαριασμό σας." + "message": "Χρησιμοποιήστε οποιοδήποτε συμβατό κλειδί ασφαλείας WebAuthn για να αποκτήσετε πρόσβαση στον λογαριασμό σας." }, "emailTitle": { "message": "Email" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Εισάγετε τον κωδικό που σας στάλθηκε στο ηλ. ταχυδρομείο." }, "loginUnavailable": { - "message": "Σύνδεση μη διαθέσιμη" + "message": "Μη διαθέσιμη σύνδεση" }, "noTwoStepProviders": { - "message": "Αυτός ο λογαριασμός έχει ενεργοποιημένη τη σύνδεση σε δύο βήματα, ωστόσο, κανένας από τους καθορισμένους παρόχους δύο βημάτων δεν υποστηρίζεται από αυτήν τη συσκευή." + "message": "Αυτός ο λογαριασμός έχει ενεργοποιημένη τη σύνδεση δύο βημάτων, ωστόσο, κανένας από τους ρυθμισμένους παρόχους δύο βημάτων δεν υποστηρίζεται από αυτήν τη συσκευή." }, "noTwoStepProviders2": { "message": "Προσθέστε επιπλέον παρόχους που υποστηρίζονται καλύτερα σε όλες τις συσκευές (όπως μια εφαρμογή επαλήθευσης)." @@ -709,19 +736,19 @@ "message": "Επιλογές σύνδεσης δύο βημάτων" }, "selfHostedEnvironment": { - "message": "Αυτο-Φιλοξενούμενο Περιβάλλον" + "message": "Αυτο-φιλοξενούμενο περιβάλλον" }, "selfHostedEnvironmentFooter": { "message": "Καθορίστε τη βασική διεύθυνση URL, της εγκατάστασης του Bitwarden που φιλοξενείται στο χώρο σας." }, "selfHostedBaseUrlHint": { - "message": "Καθορίστε τη βασική διεύθυνση URL της εγκατάστασης Bitwarden στον τομέα σας. Παράδειγμα: https://bitwarden.company.com" + "message": "Καθορίστε το βασικό URL της εγκατάστασης Bitwarden που φιλοξενείται στο χώρο σας. Παράδειγμα: https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { - "message": "Για προχωρημένη ρύθμιση, μπορείτε να ορίσετε ανεξάρτητα τη βασική διεύθυνση URL κάθε υπηρεσίας." + "message": "Για προχωρημένη παραμετροποίηση, μπορείτε να ορίσετε ανεξάρτητα το βασικό URL κάθε υπηρεσίας." }, "selfHostedEnvFormInvalid": { - "message": "You must add either the base Server URL or at least one custom environment." + "message": "Πρέπει να προσθέσετε είτε το βασικό URL του διακομιστή ή τουλάχιστον ένα προσαρμοσμένο περιβάλλον." }, "customEnvironment": { "message": "Προσαρμοσμένο περιβάλλον" @@ -733,10 +760,10 @@ "message": "URL Διακομιστή" }, "apiUrl": { - "message": "URL Διακομιστή API" + "message": "URL διακομιστή API" }, "webVaultUrl": { - "message": "URL διακομιστή web vault" + "message": "URL διακομιστή διαδικτυακής κρύπτης" }, "identityUrl": { "message": "URL διακομιστή ταυτότητας" @@ -748,7 +775,7 @@ "message": "URL διακομιστή εικονιδίων" }, "environmentSaved": { - "message": "Οι διευθύνσεις URL περιβάλλοντος έχουν αποθηκευτεί." + "message": "Τα URL περιβάλλοντος έχουν αποθηκευτεί" }, "ok": { "message": "Οκ" @@ -760,23 +787,35 @@ "message": "Όχι" }, "overwritePassword": { - "message": "Αντικατάσταση Κωδικού Πρόσβασης" + "message": "Αντικατάσταση κωδικού πρόσβασης" }, "learnMore": { "message": "Μάθετε περισσότερα" }, "featureUnavailable": { - "message": "Μη Διαθέσιμο Χαρακτηριστικό" + "message": "Μη διαθέσιμη λειτουργία" }, "loggedOut": { "message": "Αποσύνδεση" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "Έχετε αποσυνδεθεί από τον λογαριασμό σας." }, "loginExpired": { "message": "Η περίοδος σύνδεσης σας έχει λήξει." }, + "restartRegistration": { + "message": "Επανεκκίνηση εγγραφής" + }, + "expiredLink": { + "message": "Ο σύνδεσμος έληξε" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Παρακαλούμε επανεκκινήστε την εγγραφή ή δοκιμάστε να συνδεθείτε." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Μπορεί να έχετε ήδη λογαριασμό" + }, "logOutConfirmation": { "message": "Είστε βέβαιοι ότι θέλετε να αποσυνδεθείτε;" }, @@ -784,13 +823,13 @@ "message": "Αποσύνδεση" }, "addNewLogin": { - "message": "Προσθήκη Νέας Σύνδεσης" + "message": "Νέα σύνδεση" }, "addNewItem": { - "message": "Προσθήκη Νέου Στοιχείου" + "message": "Νέο αντικείμενο" }, "addNewFolder": { - "message": "Προσθήκη Νέου Φακέλου" + "message": "Νέος φάκελος" }, "view": { "message": "Προβολή" @@ -802,22 +841,22 @@ "message": "Φόρτωση..." }, "lockVault": { - "message": "Κλείδωμα Vault" + "message": "Κλείδωμα κρύπτης" }, "passwordGenerator": { - "message": "Γεννήτρια Κωδικού" + "message": "Γεννήτρια κωδικού πρόσβασης" }, "contactUs": { - "message": "Επικοινωνία" + "message": "Επικοινωνήστε μαζί μας" }, "helpAndFeedback": { "message": "Βοήθεια και σχόλια" }, "getHelp": { - "message": "Ζητήστε Βοήθεια" + "message": "Ζητήστε βοήθεια" }, "fileBugReport": { - "message": "Υποβολή Αναφοράς Σφάλματος" + "message": "Καταγράψτε μια αναφορά σφάλματος" }, "blog": { "message": "Blog" @@ -826,19 +865,19 @@ "message": "Ακολουθήστε μας" }, "syncVault": { - "message": "Συγχρονισμός Vault" + "message": "Συγχρονισμός κρύπτης" }, "changeMasterPass": { - "message": "Αλλαγή Κύριου Κωδικού" + "message": "Αλλαγή κύριου κωδικού πρόσβασης" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Συνέχεια στη διαδικτυακή εφαρμογή;" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Μπορείτε να αλλάξετε τον κύριο κωδικό πρόσβασής σας στη διαδικτυακή εφαρμογή του Bitwarden." }, "fingerprintPhrase": { - "message": "Φράση Δακτυλικών Αποτυπωμάτων", + "message": "Φράση δακτυλικών αποτυπωμάτων", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." }, "yourAccountsFingerprint": { @@ -846,13 +885,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": "Πηγαίνετε στο Web Vault" + "message": "Πηγαίνετε στη διαδικτυακή κρύπτη" }, "getMobileApp": { - "message": "Κατεβάστε την εφαρμογή για κινητά" + "message": "Απόκτηση εφαρμογής για το κινητό" }, "getBrowserExtension": { - "message": "Κατεβάστε την επέκταση προγράμματος περιήγησης" + "message": "Απόκτηση επέκτασης περιηγητή" }, "syncingComplete": { "message": "Ο συγχρονισμός ολοκληρώθηκε" @@ -861,7 +900,7 @@ "message": "Ο συγχρονισμός απέτυχε" }, "yourVaultIsLocked": { - "message": "Το vault σας είναι κλειδωμένο. Επαληθεύστε την ταυτότητά σας για να συνεχίσετε." + "message": "Η κρύπτη σας είναι κλειδωμένη. Επαληθεύστε την ταυτότητά σας για να συνεχίσετε." }, "unlock": { "message": "Ξεκλείδωμα" @@ -883,16 +922,16 @@ "message": "Μη έγκυρος κύριος κωδικός πρόσβασης" }, "twoStepLoginConfirmation": { - "message": "Η σύνδεση σε δύο βήματα καθιστά ασφαλέστερο τον λογαριασμό σας, απαιτώντας να επαληθεύσετε τα στοιχεία σας με μια άλλη συσκευή, όπως ένα κλειδί ασφαλείας, εφαρμογή επαλήθευσης ταυτότητας, SMS, τηλεφωνική κλήση, ή email. Μπορείτε να ενεργοποιήσετε τη σύνδεση σε δύο βήματα στο web vault στο bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" + "message": "Η σύνδεση δύο βημάτων καθιστά τον λογαριασμό σας πιο ασφαλή απαιτώντας από εσάς να επαληθεύσετε τη σύνδεσή σας με άλλη συσκευή, όπως ένα κλειδί ασφαλείας, μία εφαρμογή αυθεντικοποίησης, ένα SMS, μία τηλεφωνική κλήση, ή ένα μήνυμα ηλ. ταχυδρομείου. Η σύνδεση δύο βημάτων μπορεί να ρυθμιστεί στη διαδικτυακή κρύπτη bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" }, "twoStepLogin": { - "message": "Σύνδεση σε δύο βήματα" + "message": "Σύνδεση δύο βημάτων" }, "vaultTimeout": { - "message": "Χρόνος Λήξης Vault" + "message": "Χρονικό όριο λήξης κρύπτης" }, "vaultTimeoutDesc": { - "message": "Επιλέξτε πότε θα πραγματοποιείται η επιλεγμένη ενέργεια χρόνου λήξης vault." + "message": "Επιλέξτε πότε η κρύπτη σας θα αναλάβει τη δράση χρονικού ορίου λήξης κρύπτης." }, "immediately": { "message": "Άμεσα" @@ -928,16 +967,16 @@ "message": "4 ώρες" }, "onIdle": { - "message": "Κατά την Αδράνεια Συστήματος" + "message": "Σε αδράνεια συστήματος" }, "onSleep": { - "message": "Κατά την Αναμονή Συστήματος" + "message": "Κατά την αναμονή συστήματος" }, "onLocked": { - "message": "Κατά το Κλείδωμα Συστήματος" + "message": "Στο κλείδωμα συστήματος" }, "onRestart": { - "message": "Κατά την Επανεκκίνηση" + "message": "Κατά την επανεκκίνηση" }, "never": { "message": "Ποτέ" @@ -946,7 +985,7 @@ "message": "Ασφάλεια" }, "clearClipboard": { - "message": "Εκκαθάριση Πρόχειρου", + "message": "Εκκαθάριση πρόχειρου", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "clearClipboardDesc": { @@ -984,7 +1023,7 @@ "message": "Κατά το κλείσιμο του παραθύρου, εμφανίζεται ένα εικονίδιο στη γραμμή μενού." }, "enableTray": { - "message": "Ενεργοποίηση εικονιδίου περιοχής ειδοποιήσεων" + "message": "Εμφάνιση εικονιδίου στην περιοχή ειδοποιήσεων" }, "enableTrayDesc": { "message": "Να εμφανίζεται πάντα ένα εικονίδιο στην περιοχή ειδοποιήσεων." @@ -1005,7 +1044,7 @@ "message": "Εκκίνηση αυτόματα κατά τη σύνδεση" }, "openAtLoginDesc": { - "message": "Εκκίνηση της εφαρμογής Bitwarden Desktop αυτόματα κατά τη σύνδεση." + "message": "Εκκίνηση της εφαρμογής Bitwarden στην επιφάνεια εργασίας αυτόματα κατά τη σύνδεση." }, "alwaysShowDock": { "message": "Να εμφανίζεται πάντα στο Dock" @@ -1014,10 +1053,10 @@ "message": "Εμφάνιση του εικονιδίου Bitwarden στο Dock ακόμα και όταν ελαχιστοποιείται στη γραμμή μενού." }, "confirmTrayTitle": { - "message": "Επιβεβαίωση απενεργοποίησης συστήματος" + "message": "Επιβεβαίωση απόκρυψης γραμμής συστήματος" }, "confirmTrayDesc": { - "message": "Η απενεργοποίηση αυτής της ρύθμισης θα απενεργοποιήσει επίσης όλες τις άλλες ρυθμίσεις που σχετίζονται με το δίσκο." + "message": "Η απενεργοποίηση αυτής της ρύθμισης θα απενεργοποιήσει επίσης όλες τις άλλες ρυθμίσεις που σχετίζονται με τη γραμμή συστήματος." }, "language": { "message": "Γλώσσα" @@ -1044,7 +1083,7 @@ "description": "Copy to clipboard" }, "checkForUpdates": { - "message": "Έλεγχος Για Ενημερώσεις" + "message": "Έλεγχος για ενημερώσεις…" }, "version": { "message": "Έκδοση $VERSION_NUM$", @@ -1056,7 +1095,7 @@ } }, "restartToUpdate": { - "message": "Κάντε επανεκκίνηση για ενημέρωση" + "message": "Επανεκκίνηση για ενημέρωση" }, "restartToUpdateDesc": { "message": "Η έκδοση $VERSION_NUM$ είναι έτοιμη για εγκατάσταση. Θα πρέπει να επανεκκινήσετε την εφαρμογή για να ολοκληρωθεί η εγκατάσταση. Θέλετε να κάνετε επανεκκίνηση και ενημέρωση τώρα;", @@ -1068,7 +1107,7 @@ } }, "updateAvailable": { - "message": "Διαθέσιμη Ενημέρωση" + "message": "Διαθέσιμη ενημέρωση" }, "updateAvailableDesc": { "message": "Βρέθηκε μια ενημέρωση. Θέλετε να την κατεβάσετε τώρα;" @@ -1083,39 +1122,39 @@ "message": "Δεν υπάρχουν προς το παρόν διαθέσιμες ενημερώσεις. Χρησιμοποιείτε την τελευταία έκδοση." }, "updateError": { - "message": "Σφάλμα Ενημέρωσης" + "message": "Σφάλμα ενημέρωσης" }, "unknown": { "message": "Άγνωστο" }, "copyUsername": { - "message": "Αντιγραφή Ονόματος Χρήστη" + "message": "Αντιγραφή ονόματος χρήστη" }, "copyNumber": { - "message": "Αντιγραφή Αριθμού", + "message": "Αντιγραφή αριθμού", "description": "Copy credit card number" }, "copySecurityCode": { - "message": "Αντιγραφή Κωδικού Ασφαλείας", + "message": "Αντιγραφή κωδικού ασφαλείας", "description": "Copy credit card security code (CVV)" }, "premiumMembership": { "message": "Συνδρομή Premium" }, "premiumManage": { - "message": "Διαχείριση Συνδρομής" + "message": "Διαχείριση συνδρομής" }, "premiumManageAlert": { "message": "Μπορείτε να διαχειριστείτε την ιδιότητά σας ως μέλος στο bitwarden.com web vault. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" }, "premiumRefresh": { - "message": "Ανανέωση Συνδρομής" + "message": "Ανανέωση συνδρομής" }, "premiumNotCurrentMember": { - "message": "Δεν είστε premium μέλος." + "message": "Δεν είστε Premium μέλος αυτήν τη στιγμή." }, "premiumSignUpAndGet": { - "message": "Εγγραφείτε για μια premium συνδρομή και λάβετε:" + "message": "Εγγραφείτε για μια Premium συνδρομή και λάβετε:" }, "premiumSignUpStorage": { "message": "1 GB κρυπτογραφημένο αποθηκευτικό χώρο για συνημμένα αρχεία." @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Μπορείτε να αγοράσετε συνδρομή premium στο bitwarden.com web vault. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" }, + "premiumPurchaseAlertV2": { + "message": "Μπορείτε να αγοράσετε το Premium από τις ρυθμίσεις λογαριασμού σας στη διαδικτυακή εφαρμογή του Bitwarden." + }, "premiumCurrentMember": { "message": "Είστε ένα premium μέλος!" }, @@ -1160,7 +1202,7 @@ "message": "Επιτυχής ανανέωση" }, "passwordHistory": { - "message": "Ιστορικό Κωδικού" + "message": "Ιστορικό κωδικού" }, "clear": { "message": "Εκκαθάριση", @@ -1184,7 +1226,7 @@ "description": "Paste from clipboard" }, "selectAll": { - "message": "Επιλογή Όλων" + "message": "Επιλογή όλων" }, "zoomIn": { "message": "Μεγέθυνση" @@ -1193,16 +1235,16 @@ "message": "Σμίκρυνση" }, "resetZoom": { - "message": "Επαναφορά Μεγέθυνσης" + "message": "Επαναφορά μεγέθυνσης" }, "toggleFullScreen": { - "message": "Εναλλαγή σε Πλήρη Οθόνη" + "message": "Εναλλαγή πλήρους οθόνης" }, "reload": { "message": "Επαναφόρτωση" }, "toggleDevTools": { - "message": "Εναλλαγή σε Εργαλεία Προγραμματιστή" + "message": "Εναλλαγή εργαλείων προγραμματιστή" }, "minimize": { "message": "Ελαχιστοποίηση", @@ -1212,7 +1254,7 @@ "message": "Μεγέθυνση" }, "bringAllToFront": { - "message": "Φέρτε τα όλα σε πρώτο πλάνο", + "message": "Μεταφορά όλων στο προσκήνιο", "description": "Bring all windows to front (foreground)" }, "aboutBitwarden": { @@ -1225,10 +1267,10 @@ "message": "Απόκρυψη Bitwarden" }, "hideOthers": { - "message": "Απόκρυψη Άλλων" + "message": "Απόκρυψη άλλων" }, "showAll": { - "message": "Εμφάνιση Όλων" + "message": "Εμφάνιση όλων" }, "quitBitwarden": { "message": "Έξοδος Bitwarden" @@ -1243,11 +1285,14 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { - "message": "Access Token Refresh Error" + "message": "Σφάλμα Ανανέωσης Διακριτικού Πρόσβασης" }, "errorRefreshingAccessTokenDesc": { - "message": "No refresh token or API keys found. Please try logging out and logging back in." + "message": "Δε βρέθηκε διακριτικό ανανέωσης ή κλειδιά API. Παρακαλούμε δοκιμάστε να αποσυνδεθείτε και να συνδεθείτε ξανά." }, "help": { "message": "Βοήθεια" @@ -1293,7 +1338,7 @@ "description": "A programming term, also known as 'RegEx'." }, "matchDetection": { - "message": "Εντοπισμός Αντιστοίχισης", + "message": "Εντοπισμός αντιστοίχισης", "description": "URI match detection for auto-fill." }, "defaultMatchDetection": { @@ -1301,7 +1346,7 @@ "description": "Default URI match detection for auto-fill." }, "toggleOptions": { - "message": "Επιλογές Εναλλαγής" + "message": "Εναλλαγή επιλογών" }, "organization": { "message": "Οργανισμός", @@ -1318,10 +1363,10 @@ "description": "Text for a button that toggles the visibility of the window. Shows the window when it is hidden or hides the window if it is currently open." }, "hideToTray": { - "message": "Απόκρυψη στο Δίσκο" + "message": "Απόκρυψη στη γραμμή συστήματος" }, "alwaysOnTop": { - "message": "Πάντα στη Κορυφή", + "message": "Πάντα στο προσκήνιο", "description": "Application window should always stay on top of other windows" }, "dateUpdated": { @@ -1333,44 +1378,44 @@ "description": "ex. Date this item was created" }, "datePasswordUpdated": { - "message": "Ο Κωδικός Ενημερώθηκε", + "message": "Ο κωδικός ενημερώθηκε", "description": "ex. Date this password was updated" }, "exportFrom": { - "message": "Export from" + "message": "Εξαγωγή από" }, "exportVault": { - "message": "Εξαγωγή Vault" + "message": "Εξαγωγή κρύπτης" }, "fileFormat": { - "message": "Μορφή Αρχείου" + "message": "Τύπος αρχείου" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "Αυτή η εξαγωγή αρχείου θα προστατεύεται με κωδικό πρόσβασης και θα απαιτείται ο κωδικός πρόσβασης του αρχείου για αποκρυπτογράφηση." }, "filePassword": { - "message": "File password" + "message": "Κωδικός πρόσβασης αρχείου" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "Αυτός ο κωδικός πρόσβασης θα χρησιμοποιηθεί για την εξαγωγή και εισαγωγή αυτού του αρχείου" }, "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": "Χρησιμοποιήστε το κλειδί κρυπτογράφησης του λογαριασμού σας, που προέρχεται από το όνομα χρήστη και τον Κύριο Κωδικό Πρόσβασης του λογαριασμού σας, για να κρυπτογραφήσετε την εξαγωγή και να περιορίσετε την εισαγωγή μόνο στον τρέχοντα λογαριασμό Bitwarden." }, "passwordProtected": { - "message": "Password protected" + "message": "Προστατευμένο με κωδικό πρόσβασης" }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "Ορίστε έναν κωδικό πρόσβασης αρχείου για να κρυπτογραφήσετε την εξαγωγή και να τον εισάγετε σε οποιονδήποτε λογαριασμό Bitwarden χρησιμοποιώντας τον κωδικό πρόσβασης για αποκρυπτογράφηση." }, "exportTypeHeading": { - "message": "Export type" + "message": "Τύπος εξαγωγής" }, "accountRestricted": { - "message": "Account restricted" + "message": "Ο λογαριασμός περιορίστηκε" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "Το \"Κωδικός πρόσβασης αρχείου\" και το \"Επιβεβαίωση κωδικού πρόσβασης αρχείου\" δεν ταιριάζουν." }, "hCaptchaUrl": { "message": "hCaptcha Url", @@ -1411,7 +1456,7 @@ "description": "WARNING (should stay in capitalized letters if the language permits)" }, "confirmVaultExport": { - "message": "Επιβεβαίωση εξαγωγής Vault" + "message": "Επιβεβαίωση εξαγωγής κρύπτης" }, "exportWarningDesc": { "message": "Αυτή η εξαγωγή περιέχει τα δεδομένα σε μη κρυπτογραφημένη μορφή. Δεν πρέπει να αποθηκεύετε ή να στείλετε το εξαγόμενο αρχείο μέσω μη ασφαλών τρόπων (όπως μέσω email). Διαγράψτε το αμέσως μόλις τελειώσετε με τη χρήση του." @@ -1447,7 +1492,7 @@ "description": "ex. A weak password. Scale: Weak -> Good -> Strong" }, "weakMasterPassword": { - "message": "Αδύναμος Κύριος Κωδικός" + "message": "Αδύναμος κύριος κωδικός πρόσβασης" }, "weakMasterPasswordDesc": { "message": "Ο κύριος κωδικός που έχετε επιλέξει είναι αδύναμος. Θα πρέπει να χρησιμοποιήσετε έναν ισχυρό κύριο κωδικό (ή μια φράση) για την κατάλληλη προστασία του λογαριασμού Bitwarden. Είστε βέβαιοι ότι θέλετε να χρησιμοποιήσετε αυτόν τον κύριο κωδικό;" @@ -1477,9 +1522,15 @@ "additionalWindowsHelloSettings": { "message": "Πρόσθετες ρυθμίσεις του Windows Hello" }, + "unlockWithPolkit": { + "message": "Ξεκλείδωμα με αυθεντικοποίηση συστήματος" + }, "windowsHelloConsentMessage": { "message": "Επαληθεύστε για το Bitwarden." }, + "polkitConsentMessage": { + "message": "Αυθεντικοποίηση για ξεκλείδωμα του Bitwarden." + }, "unlockWithTouchId": { "message": "Ξεκλείδωμα με Touch ID" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "Να ζητείται Windows Hello κατά την εκκίνηση της εφαρμογής" }, + "autoPromptPolkit": { + "message": "Ρώτησε με για αυθεντικοποίηση συστήματος κατά την εκκίνηση" + }, "autoPromptTouchId": { "message": "Ερώτηση για το Touch ID κατά την εκκίνηση" }, @@ -1508,10 +1562,10 @@ "message": "Διαγραφή λογαριασμού" }, "deleteAccountDesc": { - "message": "Προχωρήστε παρακάτω για να διαγράψετε το λογαριασμό σας και όλα τα δεδομένα θησαυ/κιού." + "message": "Προχωρήστε παρακάτω για να διαγράψετε τον λογαριασμό σας και όλα τα δεδομένα κρύπτης." }, "deleteAccountWarning": { - "message": "Η διαγραφή του λογαριασμού σας είναι μόνιμη. Δε μπορεί να αναιρεθεί." + "message": "Η διαγραφή του λογαριασμού σας είναι μόνιμη. Δεν μπορεί να αναιρεθεί." }, "accountDeleted": { "message": "Ο λογαριασμός διαγράφηκε" @@ -1523,19 +1577,19 @@ "message": "Προτιμήσεις" }, "enableMenuBar": { - "message": "Ενεργοποίηση Εικονιδίου Μπάρας Μενού" + "message": "Εμφάνιση εικονιδίου γραμμής μενού" }, "enableMenuBarDesc": { "message": "Να εμφανίζεται πάντα το εικονίδιο στη μπάρα μενού." }, "hideToMenuBar": { - "message": "Απόκρυψη στη Μπάρα Μενού" + "message": "Απόκρυψη στη γραμμή μενού" }, "selectOneCollection": { "message": "Πρέπει να επιλέξετε τουλάχιστον μία συλλογή." }, "premiumUpdated": { - "message": "Έχετε αναβαθμίσει σε premium." + "message": "Έχετε αναβαθμιστεί σε Premium." }, "restore": { "message": "Επαναφορά" @@ -1566,16 +1620,16 @@ "message": "Μία ή περισσότερες πολιτικές του οργανισμού επηρεάζουν τις ρυθμίσεις της γεννήτριας." }, "vaultTimeoutAction": { - "message": "Ενέργεια Χρόνου Λήξης Vault" + "message": "Ενέργεια χρονικού ορίου λήξης κρύπτης" }, "vaultTimeoutActionLockDesc": { - "message": "Ένα κλειδωμένο vault απαιτεί να εισάγετε ξανά τον κύριο κωδικό για να αποκτήσετε ξανά πρόσβαση σε αυτόν." + "message": "Απαιτείται κύριος κωδικός πρόσβασης ή άλλη μέθοδος ξεκλειδώματος για να αποκτήσετε ξανά πρόσβαση στη κρύπτη σας." }, "vaultTimeoutActionLogOutDesc": { - "message": "Ένα αποσυνδεδεμένο vault απαιτεί να κάνετε ξανά έλεγχο ταυτότητας για να αποκτήσετε πρόσβαση σε αυτό." + "message": "Απαιτείται αυθεντικοποίηση για να αποκτήσετε ξανά πρόσβαση στη κρύπτη σας." }, "unlockMethodNeededToChangeTimeoutActionDesc": { - "message": "Ρυθμίστε μια μέθοδο ξεκλειδώματος για να αλλάξετε την ενέργεια χρονικού ορίου κρύπτης." + "message": "Ρυθμίστε μια μέθοδο ξεκλειδώματος για να αλλάξετε την ενέργεια χρονικού ορίου λήξης θησαυ/κίου." }, "lock": { "message": "Κλείδωμα", @@ -1586,34 +1640,34 @@ "description": "Noun: a special folder to hold deleted items" }, "searchTrash": { - "message": "Αναζήτηση Κάδου" + "message": "Αναζήτηση κάδου απορριμμάτων" }, "permanentlyDeleteItem": { - "message": "Μόνιμη Διαγραφή Αντικειμένου" + "message": "Μόνιμη διαγραφή αντικειμένου" }, "permanentlyDeleteItemConfirmation": { "message": "Είστε βέβαιοι ότι θέλετε να διαγράψετε μόνιμα αυτό το στοιχείο;" }, "permanentlyDeletedItem": { - "message": "Μόνιμα Διεγραμμένο Στοιχείο" + "message": "Το αντικείμενο διαγράφηκε οριστικά" }, "restoredItem": { - "message": "Στοιχείο που έχει Ανακτηθεί" + "message": "Το αντικείμενο επαναφέρθηκε" }, "permanentlyDelete": { - "message": "Μόνιμη Διαγραφή" + "message": "Μόνιμη διαγραφή" }, "vaultTimeoutLogOutConfirmation": { "message": "Η αποσύνδεση θα καταργήσει όλη την πρόσβαση στο vault σας και απαιτεί online έλεγχο ταυτότητας μετά το χρονικό όριο λήξης. Είστε βέβαιοι ότι θέλετε να χρησιμοποιήσετε αυτήν τη ρύθμιση;" }, "vaultTimeoutLogOutConfirmationTitle": { - "message": "Επιβεβαίωση Ενέργειας Χρονικού Ορίου" + "message": "Επιβεβαίωση ενέργειας χρονικού ορίου λήξης" }, "enterpriseSingleSignOn": { "message": "Ενιαία είσοδος για επιχειρήσεις" }, "setMasterPassword": { - "message": "Ορισμός Κύριου Κωδικού" + "message": "Ορισμός κύριου κωδικού πρόσβασης" }, "orgPermissionsUpdatedMustSetPassword": { "message": "Τα δικαιώματα του οργανισμού σας ενημερώθηκαν, απαιτώντας από εσάς να ορίσετε έναν κύριο κωδικό πρόσβασης.", @@ -1628,13 +1682,13 @@ "description": "Default title for the user verification dialog." }, "currentMasterPass": { - "message": "Τρέχων κύριος κωδικός" + "message": "Τρέχων κύριος κωδικός πρόσβασης" }, "newMasterPass": { - "message": "Νέος Κύριος Κωδικός" + "message": "Νέος κύριος κωδικός πρόσβασης" }, "confirmNewMasterPass": { - "message": "Επιβεβαίωση Νέου Κύριου Κωδικού" + "message": "Επιβεβαίωση νέου κύριου κωδικού πρόσβασης" }, "masterPasswordPolicyInEffect": { "message": "Σε μία ή περισσότερες πολιτικές του οργανισμού απαιτείται ο κύριος κωδικός να πληρεί τις ακόλουθες απαιτήσεις:" @@ -1678,20 +1732,20 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Ο νέος κύριος κωδικός δεν πληροί τις απαιτήσεις πολιτικής." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Λάβετε συμβουλές, ανακοινώσεις και ευκαιρίες έρευνας από το Bitwarden στα εισερχόμενά σας." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "Ακύρωση συνδρομής" }, "atAnyTime": { - "message": "at any time." + "message": "οποιαδήποτε στιγμή." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Συνεχίζοντας, συμφωνείτε με" }, "and": { - "message": "and" + "message": "και" }, "acceptPolicies": { "message": "Επιλέγοντας αυτό το πλαίσιο, συμφωνείτε με τα εξής:" @@ -1700,31 +1754,31 @@ "message": "Οι Όροι Παροχής Υπηρεσιών και η Πολιτική Απορρήτου δεν έχουν αναγνωριστεί." }, "enableBrowserIntegration": { - "message": "Ενεργοποίηση ενσωμάτωσης περιηγητή" + "message": "Να επιτρέπεται η ενσωμάτωση περιηγητή" }, "enableBrowserIntegrationDesc": { - "message": "Η ενσωμάτωση του προγράμματος περιήγησης χρησιμοποιείται για βιομετρικά στοιχεία στο πρόγραμμα περιήγησης." + "message": "Χρησιμοποιείται για βιομετρικά στοιχεία στον περιηγητή." }, "enableDuckDuckGoBrowserIntegration": { - "message": "Επίτρεψε ολοκλήρωση περιηγητή DuckDuckGo" + "message": "Επίτρεψε ενσωμάτωση περιηγητή DuckDuckGo" }, "enableDuckDuckGoBrowserIntegrationDesc": { - "message": "Χρησιμοποιήστε το θησαυ/κιο Bitwarden κατά την περιήγηση με το DuckDuckGo." + "message": "Χρησιμοποιήστε το θησαυ/κιο Bitwarden σας κατά την περιήγηση με το DuckDuckGo." }, "browserIntegrationUnsupportedTitle": { "message": "Η ενσωμάτωση του περιηγητή δεν υποστηρίζεται" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Σφάλμα ενεργοποίησης ενσωμάτωσης περιηγητή" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Παρουσιάστηκε σφάλμα κατά την ενεργοποίηση ενσωμάτωσης του περιηγητή." }, "browserIntegrationMasOnlyDesc": { "message": "Δυστυχώς η ενσωμάτωση του προγράμματος περιήγησης υποστηρίζεται μόνο στην έκδοση Mac App Store για τώρα." }, "browserIntegrationWindowsStoreDesc": { - "message": "Δυστυχώς η ενσωμάτωση του προγράμματος περιήγησης, δεν υποστηρίζεται στην έκδοση Windows Store." + "message": "Δυστυχώς η ενσωμάτωση του περιηγητή, δεν υποστηρίζεται προς το παρόν στην έκδοση Windows Store." }, "browserIntegrationLinuxDesc": { "message": "Δυστυχώς, η ενσωμάτωση του browser δεν υποστηρίζεται αυτήν τη στιγμή στην έκδοση linux." @@ -1733,13 +1787,13 @@ "message": "Απαιτείται επαλήθευση για ολοκλήρωση περιηγητή" }, "enableBrowserIntegrationFingerprintDesc": { - "message": "Ενεργοποιήστε ένα πρόσθετο επίπεδο ασφάλειας απαιτώντας επικύρωση φράσης δακτυλικών αποτυπωμάτων κατά τη δημιουργία μιας σύνδεσης μεταξύ της επιφάνειας εργασίας σας και του προγράμματος περιήγησης. Όταν ενεργοποιηθεί, αυτό απαιτεί παρέμβαση χρήστη και επαλήθευση κάθε φορά που δημιουργείται σύνδεση." + "message": "Προσθέστε ένα πρόσθετο επίπεδο ασφάλειας απαιτώντας επιβεβαίωση φράσης δακτυλικών αποτυπωμάτων κατά τη δημιουργία μιας σύνδεσης μεταξύ της επιφάνειας εργασίας σας και του περιηγητή σας. Αυτό απαιτεί ενέργεια χρήστη και επαλήθευση κάθε φορά που δημιουργείται μια σύνδεση." }, "enableHardwareAcceleration": { "message": "Χρήση επιτάχυνσης υλικού" }, "enableHardwareAccelerationDesc": { - "message": "Εξ ορισμού αυτή η ρύθμιση είναι ΕΝΕΡΓΗ. Απενεργοποιήστε μόνο αν αντιμετωπίζετε γραφικά προβλήματα. Απαιτείται επανεκκίνηση." + "message": "Εξ ορισμού αυτή η ρύθμιση είναι ΕΝΕΡΓΉ. Αλλάξτε σε ΑΝΕΝΕΡΓΉ μόνο αν αντιμετωπίζετε γραφικά προβλήματα. Απαιτείται επανεκκίνηση." }, "approve": { "message": "Έγκριση" @@ -1760,19 +1814,25 @@ } }, "verifyNativeMessagingConnectionDesc": { - "message": "Θα θέλατε να εγκρίνετε αυτό το αίτημα?" + "message": "Θα θέλατε να εγκρίνετε αυτό το αίτημα;" }, "verifyNativeMessagingConnectionWarning": { "message": "Εάν δεν εκκινήσατε αυτό το αίτημα, μην το εγκρίνετε." }, "biometricsNotEnabledTitle": { - "message": "Η βιομετρική δεν είναι ενεργοποιημένη" + "message": "Τα βιομετρικά στοιχεία δεν είναι ενεργοποιημένα" }, "biometricsNotEnabledDesc": { - "message": "Η βιομετρική περιήγηση απαιτεί την ενεργοποίηση των βιομετρικών στοιχείων επιφάνειας εργασίας στις ρυθμίσεις πρώτα." + "message": "Τα βιομετρικά στον περιηγητή απαιτούν την ενεργοποίηση των βιομετρικών επιφάνειας εργασίας στις ρυθμίσεις πρώτα." + }, + "biometricsManualSetupTitle": { + "message": "Η αυτόματη ρύθμιση δεν είναι διαθέσιμη" + }, + "biometricsManualSetupDesc": { + "message": "Λόγω της μεθόδου εγκατάστασης, η υποστήριξη των βιομετρικών δεν μπορεί να ενεργοποιηθεί αυτόματα. Θα θέλατε να ανοίξετε το εγχειρίδιο για το πώς να το κάνετε αυτό χειροκίνητα;" }, "personalOwnershipSubmitError": { - "message": "Λόγω μιας Πολιτικής Επιχειρήσεων, δεν επιτρέπεται η αποθήκευση στοιχείων στο προσωπικό σας vault. Αλλάξτε την επιλογή Ιδιοκτησίας σε έναν οργανισμό και επιλέξτε από τις διαθέσιμες Συλλογές." + "message": "Λόγω μιας επιχειρηματικής πολιτικής, περιορίζεστε από την αποθήκευση αντικειμένων στο ατομικό σας θησαυ/κιό. Αλλάξτε την επιλογή ιδιοκτησίας σε έναν οργανισμό και επιλέξτε από τις διαθέσιμες συλλογές." }, "hintEqualsPassword": { "message": "Η υπόδειξη κωδικού πρόσβασης, δεν μπορεί να είναι η ίδια με τον κωδικό πρόσβασης σας." @@ -1781,7 +1841,7 @@ "message": "Μια πολιτική του οργανισμού, επηρεάζει τις επιλογές ιδιοκτησίας σας." }, "personalOwnershipPolicyInEffectImports": { - "message": "Μια πολιτική οργανισμού έχει αποτρέψει την εισαγωγή στοιχείων στην προσωπική κρύπτη σας." + "message": "Μια πολιτική οργανισμού έχει αποτρέψει την εισαγωγή αντικειμένων στο ατομικό σας θησαυ/κιο." }, "allSends": { "message": "Όλα τα Sends", @@ -1802,7 +1862,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "myVault": { - "message": "Το Vault μου" + "message": "Το θησαυ/κιό μου" }, "text": { "message": "Κείμενο" @@ -1815,14 +1875,14 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "expirationDate": { - "message": "Ημερομηνία Λήξης" + "message": "Ημερομηνία λήξης" }, "expirationDateDesc": { "message": "Εάν οριστεί, η πρόσβαση σε αυτό το Send θα λήξει την καθορισμένη ημερομηνία και ώρα.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "maxAccessCount": { - "message": "Μέγιστος Αριθμός Πρόσβασης", + "message": "Μέγιστος αριθμός πρόσβασης", "description": "This text will be displayed after a Send has been accessed the maximum amount of times." }, "maxAccessCountDesc": { @@ -1830,7 +1890,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "currentAccessCount": { - "message": "Τρέχων Αριθμός Πρόσβασης" + "message": "Τρέχων αριθμός πρόσβασης" }, "disableSend": { "message": "Απενεργοποιήστε αυτό το Send έτσι ώστε κανείς να μην μπορεί να έχει πρόσβαση σε αυτό.", @@ -1849,7 +1909,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLinkLabel": { - "message": "Αποστολή Συνδέσμου", + "message": "Σύνδεσμος Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "textHiddenByDefault": { @@ -1857,26 +1917,26 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSend": { - "message": "Το Send Δημιουργήθηκε", + "message": "Το Send προστέθηκε", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editedSend": { - "message": "Το Send Επεξεργάστηκε", + "message": "Το Send αποθηκεύτηκε", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deletedSend": { - "message": "Το Send Διαγράφηκε", + "message": "Το Send διαγράφηκε", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "newPassword": { - "message": "Νέος Κωδικός" + "message": "Νέος κωδικός πρόσβασης" }, "whatTypeOfSend": { "message": "Τι είδους Send είναι αυτό;", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createSend": { - "message": "Δημιουργήστε Send", + "message": "Νέο Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTextDesc": { @@ -1912,7 +1972,7 @@ "message": "Αντιγράψτε το σύνδεσμο, για να μοιραστείτε αυτό το Send στο πρόχειρο μου, κατά την αποθήκευση." }, "sendDisabled": { - "message": "Send Απενεργοποιημένο", + "message": "Το Send αφαιρέθηκε", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDisabledWarning": { @@ -1926,13 +1986,13 @@ "message": "Απενεργοποιημένο" }, "removePassword": { - "message": "Κατάργηση κωδικού πρόσβασης" + "message": "Αφαίρεση κωδικού πρόσβασης" }, "removedPassword": { - "message": "Ο κωδικός πρόσβασης καταργήθηκε" + "message": "Ο κωδικός πρόσβασης αφαιρέθηκε" }, "removePasswordConfirmation": { - "message": "Είστε σίγουροι ότι θέλετε να καταργήσετε τον κωδικό πρόσβασης;" + "message": "Είστε σίγουροι ότι θέλετε να αφαιρέσετε τον κωδικό πρόσβασης;" }, "maxAccessCountReached": { "message": "Φτάσατε στον μέγιστο αριθμό πρόσβασης" @@ -1953,7 +2013,10 @@ "message": "Μία ή περισσότερες οργανωτικές πολιτικές επηρεάζουν τις επιλογές send σας." }, "emailVerificationRequired": { - "message": "Απαιτείται Επαλήθευση Email" + "message": "Απαιτείται επαλήθευση διεύθυνσης ηλ. ταχυδρομείου" + }, + "emailVerifiedV2": { + "message": "Η διεύθυνση ηλ. ταχυδρομείου επιβεβαιώθηκε" }, "emailVerificationRequiredDesc": { "message": "Πρέπει να επαληθεύσετε το email σας για να χρησιμοποιήσετε αυτή τη δυνατότητα." @@ -1974,10 +2037,13 @@ "message": "Ενημερώστε τον κύριο κωδικό πρόσβασης" }, "updateMasterPasswordWarning": { - "message": "Ο Κύριος Κωδικός Πρόσβασής σας άλλαξε πρόσφατα από διαχειριστή στον οργανισμό σας. Για να αποκτήσετε πρόσβαση στο vault, πρέπει να τον ενημερώσετε τώρα. Η διαδικασία θα σας αποσυνδέσει από την τρέχουσα συνεδρία σας, απαιτώντας από εσάς να συνδεθείτε ξανά. Οι ενεργές συνεδρίες σε άλλες συσκευές ενδέχεται να συνεχίσουν να είναι ενεργές για μία ώρα." + "message": "Ο κύριος κωδικός πρόσβασής σας άλλαξε πρόσφατα από έναν διαχειριστή στον οργανισμό σας. Για να αποκτήσετε πρόσβαση στο θησαυ/κιο, πρέπει να τον ενημερώσετε τώρα. Η διαδικασία θα σας αποσυνδέσει από την τρέχουσα συνεδρία σας, απαιτώντας από εσάς να συνδεθείτε ξανά. Οι ενεργές συνεδρίες σε άλλες συσκευές ενδέχεται να συνεχίσουν να είναι ενεργές για μία ώρα." }, "updateWeakMasterPasswordWarning": { - "message": "Ο κύριος κωδικός πρόσβασης δεν πληροί τις απαιτήσεις πολιτικής αυτού του οργανισμού. Για να έχετε πρόσβαση στην κρύπτη, πρέπει να ενημερώσετε τον κύριο κωδικό σας άμεσα. Η διαδικασία θα σάς αποσυνδέσει από την τρέχουσα συνεδρία σας, απαιτώντας να συνδεθείτε ξανά. Οι ενεργές συνεδρίες σε άλλες συσκευές ενδέχεται να συνεχίσουν να είναι ενεργές για το πολύ μία ώρα." + "message": "Ο κύριος κωδικός πρόσβασής σας δεν πληροί μία ή περισσότερες πολιτικές του οργανισμού σας. Για να αποκτήσετε πρόσβαση στο θησαυ/κιο, πρέπει να ενημερώσετε τον κύριο κωδικό πρόσβασής σας τώρα. Η διαδικασία θα σας αποσυνδέσει από την τρέχουσα συνεδρία σας, απαιτώντας από εσάς να συνδεθείτε ξανά. Οι ενεργές συνεδρίες σε άλλες συσκευές ενδέχεται να συνεχίσουν να είναι ενεργές για μία ώρα." + }, + "tdeDisabledMasterPasswordRequired": { + "message": "Ο οργανισμός σας έχει απενεργοποιήσει την κρυπτογράφηση αξιόπιστης συσκευής. Παρακαλώ ορίστε έναν κύριο κωδικό πρόσβασης για να αποκτήσετε πρόσβαση στο θησαυροφυλάκιο σας." }, "tryAgain": { "message": "Προσπαθήστε ξανά" @@ -2001,7 +2067,7 @@ "message": "Χρειάζεστε μια διαφορετική μέθοδο;" }, "useMasterPassword": { - "message": "Χρήση κύριου κωδικού" + "message": "Χρήση κύριου κωδικού πρόσβασης" }, "usePin": { "message": "Χρήση PIN" @@ -2010,7 +2076,7 @@ "message": "Χρήση βιομετρικών" }, "enterVerificationCodeSentToEmail": { - "message": "Εισάγετε τον κωδικό επαλήθευσης που έχει σταλεί στο email σας." + "message": "Εισάγετε τον κωδικό επαλήθευσης που έχει σταλεί στο ηλ. ταχυδρομείο σας." }, "resendCode": { "message": "Επαναποστολή κωδικού" @@ -2022,7 +2088,7 @@ "message": "Λεπτά" }, "vaultTimeoutPolicyInEffect": { - "message": "Οι πολιτικές του οργανισμού σας επηρεάζουν το χρονικό όριο vault σας. Το μέγιστο επιτρεπόμενο Χρονικό όριο Vault είναι $HOURS$ ώρα(ες) και $MINUTES$ λεπτό(ά)", + "message": "Οι πολιτικές του οργανισμού σας έχουν ορίσει το μέγιστο επιτρεπόμενο χρονικό όριο λήξης θησαυ/κίου σε $HOURS$ ώρα(ες) και $MINUTES$ λεπτό(α).", "placeholders": { "hours": { "content": "$1", @@ -2035,7 +2101,7 @@ } }, "vaultTimeoutPolicyWithActionInEffect": { - "message": "Οι πολιτικές του οργανισμού σας επηρεάζουν το χρονικό όριο λήξης της κρύ[της σας. Το μέγιστο επιτρεπόμενο χρονικό όριο λήξης vault είναι $HOURS$ ώρα(ες) και $MINUTES$ λεπτό(ά). H ενέργεια χρονικού ορίου λήξης της κρύπτης είναι ορισμένη ως $ACTION$.", + "message": "Οι πολιτικές του οργανισμού σας επηρεάζουν το χρονικό όριο λήξης του θησαυ/κίου σας. Το μέγιστο επιτρεπόμενο χρονικό όριο θησαυ/κίου είναι $HOURS$ ώρα(ες) και $MINUTES$ λεπτό(α). Η ενέργεια χρονικού ορίου λήξης του θησαυ/κίου σας έχει οριστεί σε $ACTION$.", "placeholders": { "hours": { "content": "$1", @@ -2052,7 +2118,7 @@ } }, "vaultTimeoutActionPolicyInEffect": { - "message": "Οι πολιτικές του οργανισμού σας έχουν ορίσει την ενέργεια χρονικού ορίου λήξης κρύπτης σε $ACTION$.", + "message": "Οι πολιτικές του οργανισμού σας έχουν ορίσει την ενέργεια χρονικού ορίου λήξης του θησαυ/κίου σας σε $ACTION$.", "placeholders": { "action": { "content": "$1", @@ -2063,26 +2129,29 @@ "vaultTimeoutTooLarge": { "message": "Το χρονικό όριο του vault σας υπερβαίνει τους περιορισμούς που έχει ορίσει ο οργανισμός σας." }, + "inviteAccepted": { + "message": "Η πρόσκληση έγινε αποδεκτή" + }, "resetPasswordPolicyAutoEnroll": { - "message": "Αυτόματη Εγγραφή" + "message": "Αυτόματη εγγραφή" }, "resetPasswordAutoEnrollInviteWarning": { "message": "Αυτός ο οργανισμός έχει μια επιχειρηματική πολιτική που θα σας εγγράψει αυτόματα στην επαναφορά κωδικού. Η εγγραφή θα επιτρέψει στους διαχειριστές του οργανισμού να αλλάξουν τον κύριο κωδικό πρόσβασης σας." }, "vaultExportDisabled": { - "message": "Εξαγωγή vault Απενεργοποιημένη" + "message": "Αφαίρεση εξαγωγής θησαυ/κίου" }, "personalVaultExportPolicyInEffect": { "message": "Μία ή περισσότερες οργανωτικές πολιτικές σας αποτρέπει από την εξαγωγή του προσωπικού vault." }, "addAccount": { - "message": "Προσθήκη Λογαριασμού" + "message": "Προσθήκη λογαριασμού" }, "removeMasterPassword": { - "message": "Αφαίρεση Κύριου Κωδικού Πρόσβασης" + "message": "Αφαίρεση κύριου κωδικού πρόσβασης" }, "removedMasterPassword": { - "message": "Ο κύριος κωδικός αφαιρέθηκε." + "message": "Ο κύριος κωδικός αφαιρέθηκε" }, "convertOrganizationEncryptionDesc": { "message": "$ORGANIZATION$ χρησιμοποιεί SSO με έναν αυτοεξυπηρετητή κλειδιών. Ένας κύριος κωδικός πρόσβασης δεν απαιτείται πλέον για να συνδεθείτε για τα μέλη αυτού του οργανισμού.", @@ -2103,10 +2172,10 @@ "message": "Έχετε φύγει από τον οργανισμό." }, "ssoKeyConnectorError": { - "message": "Σφάλμα Key Connector: βεβαιωθείτε ότι το Key Connector είναι διαθέσιμο και λειτουργεί σωστά." + "message": "Σφάλμα Key connector: βεβαιωθείτε ότι το Key connector είναι διαθέσιμο και λειτουργεί σωστά." }, "lockAllVaults": { - "message": "Κλείδωμα Όλων Των Vault" + "message": "Κλείδωμα όλων των θησαυ/κίων" }, "accountLimitReached": { "message": "Δεν μπορούν να συνδεθούν περισσότεροι από 5 λογαριασμοί ταυτόχρονα." @@ -2115,7 +2184,7 @@ "message": "Προτιμήσεις" }, "appPreferences": { - "message": "Ρυθμίσεις Εφαρμογής (Όλοι Οι Λογαριασμοί)" + "message": "Ρυθμίσεις εφαρμογής (όλοι οι λογαριασμοί)" }, "accountSwitcherLimitReached": { "message": "Συμπληρώθηκε το όριο λογαριασμού. Αποσυνδεθείτε από έναν λογαριασμό για να προσθέσετε έναν άλλο." @@ -2130,10 +2199,10 @@ } }, "switchAccount": { - "message": "Εναλλαγή λογαριασμού" + "message": "Αλλαγή λογαριασμού" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Έχετε ήδη λογαριασμό;" }, "options": { "message": "Ρυθμίσεις" @@ -2142,10 +2211,10 @@ "message": "Έχει λήξει το χρονικό όριο. Παρακαλώ επιστρέψτε και προσπαθήστε να συνδεθείτε ξανά." }, "exportingPersonalVaultTitle": { - "message": "Εξαγωγή Προσωπικού Vault" + "message": "Εξαγωγή ατομικού θησαυ/κίου" }, "exportingIndividualVaultDescription": { - "message": "Μόνο τα μεμονωμένα αντικείμενα κρύπτης που σχετίζονται με το $EMAIL$ θα εξαχθούν. Τα αντικείμενα κρύπτης οργανισμού δε θα συμπεριληφθούν. Μόνο πληροφορίες αντικειμένων κρύπτης θα εξαχθούν και δε θα περιλαμβάνουν συσχετιζόμενα συνημμένα.", + "message": "Μόνο τα ατομικά αντικείμενα θησαυ/κίου που σχετίζονται με το $EMAIL$ θα εξαχθούν. Τα αντικείμενα θησαυ/κίου του οργανισμού δε θα συμπεριληφθούν. Μόνο πληροφορίες αντικειμένων θησαυ/κίου θα εξαχθούν και δε θα περιλαμβάνουν συσχετιζόμενα συνημμένα.", "placeholders": { "email": { "content": "$1", @@ -2154,10 +2223,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "Εξαγωγή θησαυ/κίου οργανισμού" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Μόνο το θησαυ/κιο του οργανισμού που σχετίζεται με το $ORGANIZATION$ θα εξαχθεί. Αντικείμενα σε ατομικά θησαυ/κια ή άλλους οργανισμούς δε θα συμπεριληφθούν.", "placeholders": { "organization": { "content": "$1", @@ -2178,26 +2247,26 @@ "message": "Τι θα θέλατε να δημιουργήσετε;" }, "passwordType": { - "message": "Τύπος Κωδικού" + "message": "Τύπος κωδικού πρόσβασης" }, "regenerateUsername": { - "message": "Επαναδημιουργία Ονόματος Χρήστη" + "message": "Επαναδημιουργία ονόματος χρήστη" }, "generateUsername": { - "message": "Δημιουργία Ονόματος Χρήστη" + "message": "Δημιουργία ονόματος χρήστη" }, "usernameType": { - "message": "Τύπος Ονόματος Χρήστη" + "message": "Τύπος ονόματος χρήστη" }, "plusAddressedEmail": { - "message": "Συν Διεύθυνση Email", + "message": "Συν διεύθυνση ηλ. ταχυδρομείου", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" }, "plusAddressedEmailDesc": { "message": "Χρησιμοποιήστε τις δυνατότητες δευτερεύουσας διεύθυνσης του παρόχου email σας." }, "catchallEmail": { - "message": "Catch-all Email" + "message": "Ηλ. ταχυδρομείο κάθε σκοπού" }, "catchallEmailDesc": { "message": "Χρησιμοποιήστε τα διαμορφωμένα εισερχόμενα catch-all του domain σας." @@ -2206,31 +2275,31 @@ "message": "Τυχαίο" }, "randomWord": { - "message": "Τυχαία Λέξη" + "message": "Τυχαία λέξη" }, "websiteName": { - "message": "Όνομα Ιστοσελίδας" + "message": "Όνομα ιστοσελίδας" }, "service": { "message": "Υπηρεσία" }, "allVaults": { - "message": "Όλα τα Vaults" + "message": "Όλα τα θησαυ/κια" }, "searchOrganization": { - "message": "Αναζήτηση Οργανισμού" + "message": "Αναζήτηση οργανισμού" }, "searchMyVault": { - "message": "Αναζήτηση στο Vault" + "message": "Αναζήτηση στο θησαυ/κιό μου" }, "forwardedEmail": { - "message": "Προωθημένο Email Alias" + "message": "Προωθημένο ψευδώνυμο διεύθυνσης ηλ. ταχυδρομείου" }, "forwardedEmailDesc": { "message": "Δημιουργήστε ένα alias email με μια εξωτερική υπηρεσία προώθησης." }, "forwarderError": { - "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "message": "$SERVICENAME$ σφάλμα: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", "placeholders": { "servicename": { @@ -2244,11 +2313,11 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "Δημιουργήθηκε από το Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "Ιστοσελίδα: $WEBSITE$. Δημιουργήθηκε από το Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -2258,7 +2327,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "Μη έγκυρο $SERVICENAME$ διακριτικό API", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -2268,7 +2337,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "Μη έγκυρο $SERVICENAME$ API διακριτικό: $ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -2282,7 +2351,7 @@ } }, "forwarderNoAccountId": { - "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "message": "Αδύνατη η απόκτηση του $SERVICENAME$ καμουφλαρισμένου ID διεύθυνσης ηλ. ταχυδρομείου.", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -2292,7 +2361,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "Μη έγκυρος $SERVICENAME$ τομέας.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -2302,7 +2371,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "Μη έγκυρο $SERVICENAME$ url.", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -2312,7 +2381,7 @@ } }, "forwarderUnknownError": { - "message": "Unknown $SERVICENAME$ error occurred.", + "message": "Παρουσιάστηκε άγνωστο $SERVICENAME$ σφάλμα.", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -2322,7 +2391,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Unknown forwarder: '$SERVICENAME$'.", + "message": "Άγνωστος διαβιβαστής: '$SERVICENAME$'.", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -2348,28 +2417,28 @@ "message": "Ο οργανισμός διακόπηκε" }, "disabledOrganizationFilterError": { - "message": "Δεν είναι δυνατή η πρόσβαση σε ανασταλμένους οργανισμούς. Επικοινωνήστε με τον ιδιοκτήτη του οργανισμού σας για βοήθεια." + "message": "Δεν είναι δυνατή η πρόσβαση αντικειμένων σε ανασταλμένους οργανισμούς. Επικοινωνήστε με τον ιδιοκτήτη του οργανισμού σας για βοήθεια." }, "neverLockWarning": { - "message": "Είστε βέβαιοι ότι θέλετε να χρησιμοποιήσετε την επιλογή \"Ποτέ\"; Ο ορισμός των επιλογών κλειδώματος σε \"Ποτέ\" αποθηκεύει το κλειδί κρυπτογράφησης του vault στη συσκευή σας. Εάν χρησιμοποιήσετε αυτήν την επιλογή, θα πρέπει να διασφαλίσετε ότι θα διατηρείτε τη συσκευή σας σωστά προστατευμένη." + "message": "Είστε βέβαιοι ότι θέλετε να χρησιμοποιήσετε την επιλογή \"Ποτέ\"; Ο ορισμός των επιλογών κλειδώματος σε \"Ποτέ\" αποθηκεύει το κλειδί κρυπτογράφησης του θησαυ/κίου σας στη συσκευή σας. Εάν χρησιμοποιήσετε αυτήν την επιλογή, θα πρέπει να διασφαλίσετε ότι θα διατηρείτε τη συσκευή σας κατάλληλα προστατευμένη." }, "vault": { - "message": "Υπόγειο" + "message": "Θησαυ/κιο" }, "loginWithMasterPassword": { - "message": "Συνδεθείτε με τον κύριο κωδικό" + "message": "Συνδεθείτε με τον κύριο κωδικό πρόσβασης" }, "loggingInAs": { "message": "Σύνδεση ως" }, "rememberEmail": { - "message": "Απομνημόνευση email" + "message": "Απομνημόνευση διεύθυνσης ηλ. ταχυδρομείου" }, "notYou": { - "message": "Όχι εσείς?" + "message": "Όχι εσείς;" }, "newAroundHere": { - "message": "Νέο εδώ?" + "message": "Είστε νέος/α εδώ;" }, "loggingInTo": { "message": "Σύνδεση στο $DOMAIN$", @@ -2390,13 +2459,13 @@ "message": "Μια ειδοποίηση έχει σταλεί στη συσκευή σας." }, "fingerprintMatchInfo": { - "message": "Βεβαιωθείτε ότι το vault σας είναι ξεκλείδωτο και ότι η Φράση δακτυλικών αποτυπωμάτων ταιριάζει στην άλλη συσκευή." + "message": "Παρακαλώ βεβαιωθείτε ότι το θησαυ/κιό σας είναι ξεκλείδωτο και η φράση δακτυλικών αποτυπωμάτων ταιριάζει με την άλλη συσκευή." }, "fingerprintPhraseHeader": { "message": "Φράση δακτυλικών αποτυπωμάτων" }, "needAnotherOption": { - "message": "Η σύνδεση με χρήση συσκευής πρέπει να ρυθμιστεί στις ρυθμίσεις της εφαρμογής Bitwarden. Χρειάζεστε άλλη επιλογή;" + "message": "Η σύνδεση με τη χρήση συσκευής πρέπει να οριστεί στις ρυθμίσεις της εφαρμογής Bitwarden. Χρειάζεστε κάποια άλλη επιλογή;" }, "viewAllLoginOptions": { "message": "Δείτε όλες τις επιλογές σύνδεσης" @@ -2405,7 +2474,7 @@ "message": "Επαναποστολή ειδοποίησης" }, "toggleCharacterCount": { - "message": "Εναλλαγή μετρήσεων χαρακτήρων", + "message": "Εναλλαγή αριθμού χαρακτήρων", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, "areYouTryingtoLogin": { @@ -2482,52 +2551,52 @@ "message": "Ζητήθηκε σύνδεση" }, "creatingAccountOn": { - "message": "Creating account on" + "message": "Δημιουργία λογαριασμού στο" }, "checkYourEmail": { - "message": "Check your email" + "message": "Ελέγξτε το ηλ. ταχυδρομείο σας" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "Ακολουθήστε το σύνδεσμο στο μήνυμα ηλ. ταχυδρομείου που στάλθηκε στο" }, "andContinueCreatingYourAccount": { - "message": "and continue creating your account." + "message": "και συνεχίστε στη δημιουργία του λογαριασμού σας." }, "noEmail": { - "message": "No email?" + "message": "Κανένα μήνυμα ηλ. ταχυδρομείου;" }, "goBack": { - "message": "Go back" + "message": "Μετάβαση πίσω" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "για να επεξεργαστείτε τη διεύθυνση ηλ. ταχυδρομείου σας." }, "exposedMasterPassword": { "message": "Εκτεθειμένος Κύριος Κωδικός Πρόσβασης" }, "exposedMasterPasswordDesc": { - "message": "Ο κωδικός βρέθηκε σε μια παραβίαση δεδομένων. Χρησιμοποιήστε έναν μοναδικό κωδικό πρόσβασης για την προστασία του λογαριασμού σας. Είστε βέβαιοι ότι θέλετε να χρησιμοποιήσετε έναν εκτεθειμένο κωδικό πρόσβασης;" + "message": "Κωδικός πρόσβασης βρέθηκε σε μια διαρροή δεδομένων. Χρησιμοποιήστε έναν μοναδικό κωδικό πρόσβασης για την προστασία του λογαριασμού σας. Είστε σίγουροι ότι θέλετε να χρησιμοποιήσετε έναν εκτεθειμένο κωδικό πρόσβασης;" }, "weakAndExposedMasterPassword": { - "message": "Αδύναμος και Εκτεθειμένος Κύριος Κωδικός" + "message": "Αδύναμος και Εκτεθειμένος Κύριος Κωδικός Πρόσβασης" }, "weakAndBreachedMasterPasswordDesc": { - "message": "Αδύναμος κωδικός που έχει βρεθεί σε μια παραβίαση δεδομένων. Χρησιμοποιήστε ένα ισχυρό και μοναδικό κωδικό πρόσβασης για την προστασία του λογαριασμού σας. Είστε βέβαιοι ότι θέλετε να χρησιμοποιήσετε αυτόν τον κωδικό πρόσβασης;" + "message": "Βρέθηκε και ταυτοποιήθηκε αδύναμος κωδικός σε μια διαρροή δεδομένων. Χρησιμοποιήστε ένα ισχυρό και μοναδικό κωδικό πρόσβασης για την προστασία του λογαριασμού σας. Είστε σίγουροι ότι θέλετε να χρησιμοποιήσετε αυτόν τον κωδικό πρόσβασης;" }, "checkForBreaches": { - "message": "Ελέγξτε γνωστές παραβιάσεις δεδομένων για αυτόν τον κωδικό" + "message": "Ελέγξτε γνωστές διαρροές δεδομένων για αυτόν τον κωδικό" }, "important": { "message": "Σημαντικό:" }, "accessTokenUnableToBeDecrypted": { - "message": "You have been logged out because your access token could not be decrypted. Please log in again to resolve this issue." + "message": "Έχετε αποσυνδεθεί επειδή το διακριτικό πρόσβασής σας δεν μπορεί να αποκρυπτογραφηθεί. Παρακαλώ συνδεθείτε ξανά για να επιλύσετε αυτό το ζήτημα." }, "refreshTokenSecureStorageRetrievalFailure": { - "message": "You have been logged out because your refresh token could not be retrieved. Please log in again to resolve this issue." + "message": "Έχετε αποσυνδεθεί επειδή το διακριτικό ανανέωσης δεν μπορεί να ανακτηθεί. Παρακαλώ συνδεθείτε ξανά για να επιλύσετε αυτό το ζήτημα." }, "masterPasswordHint": { - "message": "Ο κύριος κωδικός πρόσβασης δεν μπορεί να ανακτηθεί εάν τον ξεχάσετε!" + "message": "Ο κύριος κωδικός πρόσβασης σας δεν μπορεί να ανακτηθεί εάν τον ξεχάσετε!" }, "characterMinimum": { "message": "Τουλάχιστον $LENGTH$ χαρακτήρες", @@ -2539,7 +2608,7 @@ } }, "windowsBiometricUpdateWarning": { - "message": "Το Bitwarden συστήνει την ενημέρωση των βιομετρικών ρυθμίσεών σας ώστε να απαιτηθεί ο κύριος κωδικός πρόσβασης (ή PIN) στο πρώτο ξεκλείδωμα. Θέλετε να ενημερώσετε τις ρυθμίσεις σας τώρα;" + "message": "Το Bitwarden συστήνει την ενημέρωση των βιομετρικών ρυθμίσεών σας ώστε να απαιτείται ο κύριος κωδικός πρόσβασης (ή PIN) στο πρώτο ξεκλείδωμα. Θέλετε να ενημερώσετε τις ρυθμίσεις σας τώρα;" }, "windowsBiometricUpdateWarningTitle": { "message": "Ενημέρωση Προτεινόμενων Ρυθμίσεων" @@ -2560,7 +2629,7 @@ "message": "Αίτηση έγκρισης διαχειριστή" }, "approveWithMasterPassword": { - "message": "Έγκριση με τον κύριο κωδικό" + "message": "Έγκριση με τον κύριο κωδικό πρόσβασης" }, "region": { "message": "Περιοχή" @@ -2600,13 +2669,13 @@ "message": "Η σύνδεση εγκρίθηκε" }, "userEmailMissing": { - "message": "Το email του χρήστη λείπει" + "message": "Η διεύθυνση ηλ. ταχυδρομείου του χρήστη λείπει" }, "deviceTrusted": { "message": "Αξιόπιστη συσκευή" }, "inputRequired": { - "message": "Απαιτείται είσοδος." + "message": "Απαιτείται καταχώρηση." }, "required": { "message": "απαιτείται" @@ -2615,7 +2684,7 @@ "message": "Αναζήτηση" }, "inputMinLength": { - "message": "Η είσοδος πρέπει να είναι τουλάχιστον $COUNT$ χαρακτήρες.", + "message": "Η καταχώρηση πρέπει να είναι τουλάχιστον $COUNT$ χαρακτήρες.", "placeholders": { "count": { "content": "$1", @@ -2624,7 +2693,7 @@ } }, "inputMaxLength": { - "message": "Η είσοδος δεν πρέπει να υπερβαίνει τους $COUNT$ χαρακτήρες.", + "message": "Η καταχώρηση δεν πρέπει να υπερβαίνει τους $COUNT$ χαρακτήρες.", "placeholders": { "count": { "content": "$1", @@ -2642,7 +2711,7 @@ } }, "inputMinValue": { - "message": "Η τιμή εισόδου πρέπει να είναι τουλάχιστον $MIN$.", + "message": "Η τιμή καταχώρησης πρέπει να είναι τουλάχιστον $MIN$.", "placeholders": { "min": { "content": "$1", @@ -2651,7 +2720,7 @@ } }, "inputMaxValue": { - "message": "Η τιμή εισόδου δεν πρέπει να υπερβαίνει το $MAX$.", + "message": "Η τιμή καταχώρησης δεν πρέπει να υπερβαίνει το $MAX$.", "placeholders": { "max": { "content": "$1", @@ -2660,14 +2729,14 @@ } }, "multipleInputEmails": { - "message": "1 ή περισσότερα email δεν είναι έγκυρα" + "message": "1 ή περισσότερες διευθύνσεις ηλ. ταχυδρομείου δεν είναι έγκυρες" }, "inputTrimValidator": { - "message": "Η είσοδος δεν πρέπει να περιέχει μόνο κενά.", + "message": "Η καταχώρηση δεν πρέπει να περιέχει μόνο κενά.", "description": "Notification to inform the user that a form's input can't contain only whitespace." }, "inputEmail": { - "message": "Η είσοδος δεν είναι διεύθυνση email." + "message": "Η καταχώρηση δεν είναι διεύθυνση ηλ. ταχυδρομείου." }, "fieldsNeedAttention": { "message": "$COUNT$ πεδίο(α) παραπάνω χρειάζονται την προσοχή σας.", @@ -2706,7 +2775,7 @@ "message": "Υπομενού" }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "Εναλλαγή πλευρικής πλοήγησης" }, "skipToContent": { "message": "Μετάβαση στο περιεχόμενο" @@ -2715,10 +2784,10 @@ "message": "Κλειδί πρόσβασης" }, "passkeyNotCopied": { - "message": "Το κλειδί πρόσβασης δεν θα αντιγραφεί" + "message": "Το κλειδί πρόσβασης δε θα αντιγραφεί" }, "passkeyNotCopiedAlert": { - "message": "Το κλειδί πρόσβασης δε θα αντιγραφεί στο κλωνοποιημένο στοιχείο. Θέλετε να συνεχίσετε την κλωνοποίηση αυτού του στοιχείου;" + "message": "Το κλειδί πρόσβασης δε θα αντιγραφεί στο κλωνοποιημένο αντικείμενο. Θέλετε να συνεχίσετε την κλωνοποίηση αυτού του αντικειμένου;" }, "aliasDomain": { "message": "Ψευδώνυμο τομέα" @@ -2731,7 +2800,7 @@ "message": "Σφάλμα κατά την εισαγωγή" }, "importErrorDesc": { - "message": "Παρουσιάστηκε πρόβλημα με τα δεδομένα που επιχειρήσατε να εισαγάγετε. Παρακαλώ επιλύστε τα σφάλματα που αναφέρονται παρακάτω στο αρχείο πηγής και προσπαθήστε ξανά." + "message": "Παρουσιάστηκε πρόβλημα με τα δεδομένα που επιχειρήσατε να εισαγάγετε. Παρακαλώ επιλύστε τα σφάλματα που αναφέρονται παρακάτω στο πηγαίο αρχείο και προσπαθήστε ξανά." }, "resolveTheErrorsBelowAndTryAgain": { "message": "Επιλύστε τα παρακάτω σφάλματα και προσπαθήστε ξανά." @@ -2755,7 +2824,7 @@ "message": "Σύνολο" }, "importWarning": { - "message": "Εισαγάγετε δεδομένα στην $ORGANIZATION$. Τα δεδομένα σας μπορεί να μοιραστούν με μέλη αυτού του οργανισμού. Θέλετε να συνεχίσετε;", + "message": "Εισαγάγετε δεδομένα στο $ORGANIZATION$. Τα δεδομένα σας μπορεί να μοιραστούν με μέλη αυτού του οργανισμού. Θέλετε να συνεχίσετε;", "placeholders": { "organization": { "content": "$1", @@ -2763,29 +2832,32 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Σφάλμα κατά τη σύνδεση με την υπηρεσία Duo. Χρησιμοποιήστε μια διαφορετική μέθοδο σύνδεσης δύο βημάτων ή επικοινωνήστε με την Duo για βοήθεια." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Εκκινήστε το Duo και ακολουθήστε τα βήματα για να ολοκληρώσετε τη σύνδεση." }, "duoRequiredByOrgForAccount": { - "message": "Η Σύνδεση δύο βημάτων Duo απαιτείται για τον λογαριασμό σας." + "message": "Η σύνδεση δύο βημάτων Duo απαιτείται για τον λογαριασμό σας." }, "launchDuo": { "message": "Εκκίνηση Duo στον περιηγητή" }, "importFormatError": { - "message": "Τα δεδομένα δεν έχουν διαμορφωθεί σωστά. Ελέγξτε το αρχείο εισαγωγής και δοκιμάστε ξανά." + "message": "Τα δεδομένα δεν είναι σωστά διαμορφωμένα. Παρακαλώ ελέγξτε το αρχείο εισαγωγής σας και δοκιμάστε ξανά." }, "importNothingError": { "message": "Τίποτα δεν εισήχθη." }, "importEncKeyError": { - "message": "Σφάλμα αποκρυπτογράφησης του εξαγόμενου αρχείου. Το κλειδί κρυπτογράφησης δεν ταιριάζει με το κλειδί κρυπτογράφησης που χρησιμοποιήθηκε για την εξαγωγή των δεδομένων." + "message": "Σφάλμα αποκρυπτογράφησης του εξαγόμενου αρχείου. Το κλειδί κρυπτογράφησης σας δεν ταιριάζει με το κλειδί κρυπτογράφησης που χρησιμοποιήθηκε για την εξαγωγή των δεδομένων." }, "invalidFilePassword": { "message": "Μη έγκυρος κωδικός πρόσβασης, παρακαλώ χρησιμοποιήστε τον κωδικό πρόσβασης που εισαγάγατε όταν δημιουργήσατε το αρχείο εξαγωγής." }, - "importDestination": { - "message": "Προορισμός εισαγωγής" + "destination": { + "message": "Προορισμός" }, "learnAboutImportOptions": { "message": "Μάθετε για τις επιλογές εισαγωγής σας" @@ -2797,7 +2869,7 @@ "message": "Επιλέξτε μια συλλογή" }, "importTargetHint": { - "message": "Επιλέξτε αυτό αν θέλετε τα περιεχόμενα του εισαγόμενου αρχείου να μετακινηθούν σε $DESTINATION$", + "message": "Επιλέξτε αυτή την επιλογή εάν θέλετε τα περιεχόμενα του εισαγόμενου αρχείου να μετακινηθούν σε $DESTINATION$", "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", "placeholders": { "destination": { @@ -2807,10 +2879,10 @@ } }, "importUnassignedItemsError": { - "message": "Το αρχείο περιέχει μη συσχετισμένα στοιχεία." + "message": "Το αρχείο περιέχει μη αναθετημένα αντικείμενα." }, "selectFormat": { - "message": "Επιλέξτε τη μορφή του αρχείου εισαγωγής" + "message": "Επιλέξτε τον τύπο του αρχείου εισαγωγής" }, "selectImportFile": { "message": "Επιλέξτε το αρχείο εισαγωγής" @@ -2835,7 +2907,7 @@ } }, "confirmVaultImport": { - "message": "Επιβεβαίωση εισαγωγής κρύπτης" + "message": "Επιβεβαίωση εισαγωγής θησαυ/κίου" }, "confirmVaultImportDesc": { "message": "Αυτό το αρχείο προστατεύεται με κωδικό πρόσβασης. Παρακαλώ εισαγάγετε τον κωδικό πρόσβασης για την εισαγωγή δεδομένων." @@ -2844,19 +2916,19 @@ "message": "Επιβεβαίωση κωδικού πρόσβασης αρχείου" }, "exportSuccess": { - "message": "Vault data exported" + "message": "Εξήχθησαν τα δεδομένα θησαυ/κίου" }, "multifactorAuthenticationCancelled": { - "message": "Ο πολυμερής έλεγχος ταυτότητας ακυρώθηκε" + "message": "Η αυθεντικοποίηση πολλών παραγόντων ακυρώθηκε" }, "noLastPassDataFound": { - "message": "Δεν βρέθηκαν δεδομένα LastPass" + "message": "Δε βρέθηκαν δεδομένα LastPass" }, "incorrectUsernameOrPassword": { - "message": "Λάθος όνομα χρήστη ή κωδικού πρόσβασης" + "message": "Λάθος όνομα χρήστη ή κωδικός πρόσβασης" }, "incorrectPassword": { - "message": "Λάθος κωδικός" + "message": "Λάθος κωδικός πρόσβασης" }, "incorrectCode": { "message": "Λάθος κωδικός" @@ -2865,25 +2937,25 @@ "message": "Λάθος PIN" }, "multifactorAuthenticationFailed": { - "message": "Ο πολυμερής έλεγχος ταυτότητας απέτυχε" + "message": "Η αυθεντικοποίηση πολλών παραγόντων απέτυχε" }, "includeSharedFolders": { "message": "Συμπερίληψη κοινόχρηστων φακέλων" }, "lastPassEmail": { - "message": "LastPass Email" + "message": "Διεύθυνση ηλ. ταχυδρομείου LastPass" }, "importingYourAccount": { "message": "Εισαγωγή του λογαριασμού σας..." }, "lastPassMFARequired": { - "message": "Απαιτείται πολυμερής ταυτοποίηση LastPass" + "message": "Απαιτείται αυθεντικοποίηση πολλών παραγόντων LastPass" }, "lastPassMFADesc": { - "message": "Εισαγάγετε τον κωδικό μιας χρήσης από την εφαρμογή επαλήθευσης" + "message": "Εισαγάγετε τον κωδικό μιας χρήσης από την εφαρμογή αυθεντικοποίησης" }, "lastPassOOBDesc": { - "message": "Εγκρίνετε το αίτημα σύνδεσης στην εφαρμογή επαλήθευσης ή εισαγάγετε έναν κωδικό πρόσβασης μιας χρήσης." + "message": "Εγκρίνετε το αίτημα σύνδεσης στην εφαρμογή αυθεντικοποίησής σας ή εισαγάγετε έναν κωδικό πρόσβασης μιας χρήσης." }, "passcode": { "message": "Κωδικός" @@ -2892,16 +2964,16 @@ "message": "Κύριος κωδικός πρόσβασης LastPass" }, "lastPassAuthRequired": { - "message": "Απαιτείται ταυτοποίηση LastPass" + "message": "Απαιτείται αυθεντικοποίηση LastPass" }, "awaitingSSO": { - "message": "Αναμονή ελέγχου ταυτότητας SSO" + "message": "Αναμονή αυθεντικοποίησης SSO" }, "awaitingSSODesc": { "message": "Παρακαλούμε συνεχίστε τη σύνδεση χρησιμοποιώντας τα στοιχεία της εταιρείας σας." }, "seeDetailedInstructions": { - "message": "Δείτε λεπτομερείς οδηγίες στην ιστοσελίδα βοήθειας μας στο", + "message": "Δείτε λεπτομερείς οδηγίες στη βοηθητική ιστοσελίδα μας στο", "description": "This is followed a by a hyperlink to the help website." }, "importDirectlyFromLastPass": { @@ -2911,23 +2983,23 @@ "message": "Εισαγωγή από CSV" }, "lastPassTryAgainCheckEmail": { - "message": "Δοκιμάστε ξανά ή ψάξτε για ένα email από το LastPass για να επιβεβαιώσετε ότι είστε εσείς." + "message": "Δοκιμάστε ξανά ή ψάξτε για ένα μήνυμα ηλ. ταχυδρομείου από το LastPass για να επιβεβαιώσετε ότι είστε εσείς." }, "collection": { "message": "Συλλογή" }, "lastPassYubikeyDesc": { - "message": "Εισαγάγετε το YubiKey που σχετίζεται με το λογαριασμό LastPass στη θύρα USB του υπολογιστή σας και στη συνέχεια αγγίξτε το κουμπί του." + "message": "Εισάγετε το YubiKey που σχετίζεται με τον λογαριασμό LastPass στη θύρα USB του υπολογιστή σας και στη συνέχεια αγγίξτε το κουμπί του." }, "commonImportFormats": { - "message": "Κοινές μορφές", + "message": "Κοινοί τύποι", "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Επιτυχία" }, "troubleshooting": { - "message": "Αντιμετώπιση Προβλημάτων" + "message": "Αντιμετώπιση προβλημάτων" }, "disableHardwareAccelerationRestart": { "message": "Απενεργοποίηση επιτάχυνσης υλικού και επανεκκίνηση" @@ -2936,19 +3008,19 @@ "message": "Ενεργοποίηση επιτάχυνσης υλικού και επανεκκίνηση" }, "removePasskey": { - "message": "Remove passkey" + "message": "Αφαίρεση κλειδιού πρόσβασης" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Το κλειδί πρόσβασης αφαιρέθηκε" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Σφάλμα κατά την ανάθεση συλλογής προορισμού." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Σφάλμα κατά την ανάθεση φακέλου προορισμού." }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "Προβολή αντικειμένων στο $NAME$", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -2958,7 +3030,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "Πίσω στο $NAME$", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -2968,11 +3040,11 @@ } }, "back": { - "message": "Back", + "message": "Πίσω", "description": "Button text to navigate back" }, "removeItem": { - "message": "Remove $NAME$", + "message": "Αφαίρεση $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Δεδομένα" + }, + "fileSends": { + "message": "Send αρχείων" + }, + "textSends": { + "message": "Send κειμένων" + }, + "ssoError": { + "message": "Δεν βρέθηκαν ελεύθερες θύρες για τη σύνδεση sso." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 4781042413d..5991fc4d06d 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Settings" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -717,10 +744,10 @@ "selfHostedBaseUrlHint": { "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" }, - "selfHostedCustomEnvHeader" :{ + "selfHostedCustomEnvHeader": { "message": "For advanced configuration, you can specify the base URL of each service independently." }, - "selfHostedEnvFormInvalid" :{ + "selfHostedEnvFormInvalid": { "message": "You must add either the base Server URL or at least one custom environment." }, "customEnvironment": { @@ -777,6 +804,18 @@ "loginExpired": { "message": "Your login session has expired." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a premium member!" }, @@ -1243,10 +1285,13 @@ } } }, - "errorRefreshingAccessToken":{ + "copySuccessful": { + "message": "Copy Successful" + }, + "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, - "errorRefreshingAccessTokenDesc":{ + "errorRefreshingAccessTokenDesc": { "message": "No refresh token or API keys found. Please try logging out and logging back in." }, "help": { @@ -1353,7 +1398,7 @@ }, "exportPasswordDescription": { "message": "This password will be used to export and import this file" - }, + }, "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." }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1579,7 +1633,7 @@ }, "lock": { "message": "Lock", - "description": "Verb form: to make secure or inaccesible by" + "description": "Verb form: to make secure or inaccessible by" }, "trash": { "message": "Trash", @@ -1623,7 +1677,7 @@ "message": "Your organization requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, - "verificationRequired" : { + "verificationRequired": { "message": "Verification required", "description": "Default title for the user verification dialog." }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,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 organization and choose from available collections." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 63d9cafd136..52a3b3bcfd2 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organisation" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organisation by setting a master password." + }, "settings": { "message": "Settings" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Your login session has expired." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a premium member!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,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." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organisation." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrolment" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 6fa8989e863..5e9ba349580 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organisation" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organisation by setting a master password." + }, "settings": { "message": "Settings" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Your login session has expired." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a premium member!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,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." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email Verification Required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index cbea97186fd..3967a01d894 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -6,7 +6,7 @@ "message": "Filtriloj" }, "allItems": { - "message": "Ĉiuj Eroj" + "message": "Ĉiuj eroj" }, "favorites": { "message": "Plej ŝatataj" @@ -15,13 +15,13 @@ "message": "Tipoj" }, "typeLogin": { - "message": "Saluto" + "message": "Ensaluti" }, "typeCard": { "message": "Karto" }, "typeIdentity": { - "message": "Idento" + "message": "Identeco" }, "typeSecureNote": { "message": "Sekura noto" @@ -33,22 +33,22 @@ "message": "Kolektoj" }, "searchVault": { - "message": "Traserĉu trezorejon" + "message": "Traserĉi trezorejon" }, "addItem": { - "message": "Aldoni elementon" + "message": "Aldoni eron" }, "shared": { - "message": "Kundividita" + "message": "Konigita" }, "share": { - "message": "Kundividi" + "message": "Konigi" }, "moveToOrganization": { - "message": "Movu al organizo" + "message": "Movi al organizaĵo" }, "movedItemToOrg": { - "message": "$ITEMNAME$ moviĝis al $ORGNAME$", + "message": "$ITEMNAME$ movita al $ORGNAME$", "placeholders": { "itemname": { "content": "$1", @@ -61,13 +61,13 @@ } }, "moveToOrgDesc": { - "message": "Elektu organizon kun kiu vi volas dividi ĉi tiun eron. Dividado transdonas posedon de la ero al la organizo. Vi ne plu estos la rekta posedanto de ĉi tiu ero post kiam ĝi estos dividita." + "message": "Elektu organizaĵon, al kiu vi volas movi ĉi tiun eron. Movado al organizaĵo transdonas la posedon de la ero al tiu organizaĵo. Vi ne plu estos la rekta posedanto de la ero post kiam ĝi estos movita." }, "attachments": { "message": "Aldonaĵoj" }, "viewItem": { - "message": "Vidi la elementon" + "message": "Vidi la eron" }, "name": { "message": "Nomo" @@ -98,10 +98,10 @@ "message": "Pasfrazo" }, "editItem": { - "message": "Redakti la elementon" + "message": "Redakti la eron" }, "emailAddress": { - "message": "Retpoŝta adreso" + "message": "Retpoŝtadreso" }, "verificationCodeTotp": { "message": "Kontrola kodo (TOTP)" @@ -116,10 +116,10 @@ "message": "Propraj kampoj" }, "launch": { - "message": "Lanĉo" + "message": "Lanĉi" }, "copyValue": { - "message": "Kopii valoron", + "message": "Kopii la valoron", "description": "Copy value to clipboard" }, "minimizeOnCopyToClipboard": { @@ -151,7 +151,7 @@ "message": "Kodo de sekureco" }, "identityName": { - "message": "Nomo de la identilo" + "message": "Identecnomo" }, "company": { "message": "Kompanio" @@ -166,10 +166,10 @@ "message": "License number" }, "email": { - "message": "Email" + "message": "Retpoŝtadreso" }, "phone": { - "message": "Telefono" + "message": "Telefonnumero" }, "address": { "message": "Adreso" @@ -184,7 +184,7 @@ "message": "An error has occurred." }, "error": { - "message": "Error" + "message": "Eraro" }, "january": { "message": "januaro" @@ -211,19 +211,19 @@ "message": "aŭgusto" }, "september": { - "message": "September" + "message": "septembro" }, "october": { - "message": "October" + "message": "oktobro" }, "november": { - "message": "November" + "message": "novembro" }, "december": { - "message": "December" + "message": "decembro" }, "ex": { - "message": "ex.", + "message": "ekz.", "description": "Short abbreviation for 'example'." }, "title": { @@ -239,7 +239,7 @@ "message": "S-ino" }, "mx": { - "message": "Mx" + "message": "Ges-ro" }, "dr": { "message": "Dr-o" @@ -251,16 +251,16 @@ "message": "Expiration year" }, "select": { - "message": "Select" + "message": "Elekti" }, "other": { - "message": "Other" + "message": "Alia" }, "generatePassword": { "message": "Generate password" }, "type": { - "message": "Type" + "message": "Tipo" }, "firstName": { "message": "First name" @@ -272,16 +272,16 @@ "message": "Last name" }, "fullName": { - "message": "Full name" + "message": "Plena nomo" }, "address1": { - "message": "Address 1" + "message": "Adreso 1" }, "address2": { - "message": "Address 2" + "message": "Adreso 2" }, "address3": { - "message": "Address 3" + "message": "Adreso 3" }, "cityTown": { "message": "City / Town" @@ -290,25 +290,25 @@ "message": "State / Province" }, "zipPostalCode": { - "message": "Zip / Postal code" + "message": "Poŝtkodo" }, "country": { - "message": "Country" + "message": "Lando" }, "save": { - "message": "Save" + "message": "Konservi" }, "cancel": { - "message": "Cancel" + "message": "Nuligi" }, "delete": { - "message": "Delete" + "message": "Forigi" }, "favorite": { "message": "Favorite" }, "edit": { - "message": "Edit" + "message": "Redakti" }, "authenticatorKeyTotp": { "message": "Authenticator key (TOTP)" @@ -320,22 +320,22 @@ "message": "New custom field" }, "value": { - "message": "Value" + "message": "Valoro" }, "dragToSort": { "message": "Drag to sort" }, "cfTypeText": { - "message": "Text" + "message": "Teksto" }, "cfTypeHidden": { - "message": "Hidden" + "message": "Kaŝita" }, "cfTypeBoolean": { - "message": "Boolean" + "message": "Bulea" }, "cfTypeLinked": { - "message": "Linked", + "message": "Ligita", "description": "This describes a field that is 'linked' (related) to another field." }, "linkedValue": { @@ -343,7 +343,7 @@ "description": "This describes a value that is 'linked' (related) to another value." }, "remove": { - "message": "Remove" + "message": "Forigi" }, "nameRequired": { "message": "Name is required." @@ -432,7 +432,7 @@ "message": "Include number" }, "close": { - "message": "Close" + "message": "Fermi" }, "minNumbers": { "message": "Minimum numbers" @@ -470,7 +470,7 @@ "message": "Attachment saved" }, "file": { - "message": "File" + "message": "Dosiero" }, "selectFile": { "message": "Select a file" @@ -506,7 +506,7 @@ "message": "Finish creating your account by setting a password" }, "logIn": { - "message": "Log in" + "message": "Ensaluti" }, "submit": { "message": "Submit" @@ -539,8 +539,26 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { - "message": "Settings" + "message": "Agordoj" }, "passwordHint": { "message": "Password hint" @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -615,11 +639,14 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, "continue": { - "message": "Continue" + "message": "Daŭrigi" }, "enterVerificationCodeApp": { "message": "Enter the 6 digit verification code from your authenticator app." @@ -751,13 +778,13 @@ "message": "Environment URLs saved" }, "ok": { - "message": "Ok" + "message": "Bone" }, "yes": { - "message": "Yes" + "message": "Jes" }, "no": { - "message": "No" + "message": "Ne" }, "overwritePassword": { "message": "Overwrite password" @@ -769,7 +796,7 @@ "message": "Feature unavailable" }, "loggedOut": { - "message": "Adiaŭita" + "message": "Elsalutinta" }, "loggedOutDesc": { "message": "You have been logged out of your account." @@ -777,32 +804,44 @@ "loginExpired": { "message": "Your login session has expired." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, "logOut": { - "message": "Adiaŭi" + "message": "Elsaluti" }, "addNewLogin": { "message": "New login" }, "addNewItem": { - "message": "New item" + "message": "Nova ero" }, "addNewFolder": { "message": "New folder" }, "view": { - "message": "View" + "message": "Vido" }, "account": { - "message": "Account" + "message": "Konto" }, "loading": { - "message": "Loading..." + "message": "Ŝargado..." }, "lockVault": { - "message": "Lock vault" + "message": "Ŝlosi la trezorejon" }, "passwordGenerator": { "message": "Password generator" @@ -820,10 +859,10 @@ "message": "File a bug report" }, "blog": { - "message": "Blog" + "message": "Blogo" }, "followUs": { - "message": "Follow us" + "message": "Sekvi nin" }, "syncVault": { "message": "Sync vault" @@ -861,13 +900,13 @@ "message": "Syncing failed" }, "yourVaultIsLocked": { - "message": "Your vault is locked. Verify your identity to continue." + "message": "Via trezorejo estas ŝlosita. Kontrolu vian identecon por daŭrigi." }, "unlock": { - "message": "Unlock" + "message": "Malŝlosi" }, "loggedInAsOn": { - "message": "Logged in as $EMAIL$ on $HOSTNAME$.", + "message": "Ensalutinta kiel $EMAIL$ ĉe $HOSTNAME$.", "placeholders": { "email": { "content": "$1", @@ -895,37 +934,37 @@ "message": "Choose when your vault will take the vault timeout action." }, "immediately": { - "message": "Immediately" + "message": "Tuj" }, "tenSeconds": { - "message": "10 seconds" + "message": "10 sekundoj" }, "twentySeconds": { - "message": "20 seconds" + "message": "20 sekundoj" }, "thirtySeconds": { - "message": "30 seconds" + "message": "30 sekundoj" }, "oneMinute": { - "message": "1 minute" + "message": "1 minuto" }, "twoMinutes": { - "message": "2 minutes" + "message": "2 minutoj" }, "fiveMinutes": { - "message": "5 minutes" + "message": "5 minutoj" }, "fifteenMinutes": { - "message": "15 minutes" + "message": "15 minutoj" }, "thirtyMinutes": { - "message": "30 minutes" + "message": "30 minutoj" }, "oneHour": { - "message": "1 hour" + "message": "1 horo" }, "fourHours": { - "message": "4 hours" + "message": "4 horoj" }, "onIdle": { "message": "On system idle" @@ -940,10 +979,10 @@ "message": "On restart" }, "never": { - "message": "Never" + "message": "Neniam" }, "security": { - "message": "Security" + "message": "Sekureco" }, "clearClipboard": { "message": "Clear clipboard", @@ -1020,27 +1059,27 @@ "message": "Turning off this setting will also turn off all other tray related settings." }, "language": { - "message": "Language" + "message": "Lingvo" }, "languageDesc": { "message": "Change the language used by the application. Restart is required." }, "theme": { - "message": "Theme" + "message": "Etoso" }, "themeDesc": { "message": "Change the application's color theme." }, "dark": { - "message": "Dark", + "message": "Malhela", "description": "Dark color" }, "light": { - "message": "Light", + "message": "Hela", "description": "Light color" }, "copy": { - "message": "Copy", + "message": "Kopii", "description": "Copy to clipboard" }, "checkForUpdates": { @@ -1077,7 +1116,7 @@ "message": "Restart" }, "later": { - "message": "Later" + "message": "Poste" }, "noUpdatesAvailable": { "message": "No updates are currently available. You are using the latest version." @@ -1086,7 +1125,7 @@ "message": "Update error" }, "unknown": { - "message": "Unknown" + "message": "Nekonata" }, "copyUsername": { "message": "Copy username" @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a premium member!" }, @@ -1160,7 +1202,7 @@ "message": "Refresh complete" }, "passwordHistory": { - "message": "Password history" + "message": "Pasvorta historio" }, "clear": { "message": "Clear", @@ -1170,27 +1212,27 @@ "message": "There are no passwords to list." }, "undo": { - "message": "Undo" + "message": "Malfari" }, "redo": { - "message": "Redo" + "message": "Refari" }, "cut": { - "message": "Cut", + "message": "Eltondi", "description": "Cut to clipboard" }, "paste": { - "message": "Paste", + "message": "Alglui", "description": "Paste from clipboard" }, "selectAll": { - "message": "Select all" + "message": "Elekti ĉion" }, "zoomIn": { - "message": "Zoom in" + "message": "Zomi" }, "zoomOut": { - "message": "Zoom out" + "message": "Malzomi" }, "resetZoom": { "message": "Reset zoom" @@ -1209,17 +1251,17 @@ "description": "Minimize window" }, "zoom": { - "message": "Zoom" + "message": "Zomi" }, "bringAllToFront": { "message": "Bring all to front", "description": "Bring all windows to front (foreground)" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "Pri Bitwarden" }, "services": { - "message": "Services" + "message": "Servoj" }, "hideBitwarden": { "message": "Hide Bitwarden" @@ -1234,7 +1276,7 @@ "message": "Quit Bitwarden" }, "valueCopied": { - "message": "$VALUE$ copied", + "message": "$VALUE$ kopiita", "description": "Value has been copied to the clipboard.", "placeholders": { "value": { @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1250,10 +1295,10 @@ "message": "No refresh token or API keys found. Please try logging out and logging back in." }, "help": { - "message": "Help" + "message": "Helpo" }, "window": { - "message": "Window" + "message": "Fenestro" }, "checkPassword": { "message": "Check if password has been exposed." @@ -1279,7 +1324,7 @@ "description": "Domain name. Ex. website.com" }, "host": { - "message": "Host", + "message": "Gastiganto", "description": "A URL's host value. For example, the host of https://sub.domain.com:443 is 'sub.domain.com:443'." }, "exact": { @@ -1308,10 +1353,10 @@ "description": "An entity of multiple related people (ex. a team or business organization)." }, "default": { - "message": "Default" + "message": "Defaŭlta" }, "exit": { - "message": "Exit" + "message": "Forlasi" }, "showHide": { "message": "Show / Hide", @@ -1329,7 +1374,7 @@ "description": "ex. Date this item was updated" }, "dateCreated": { - "message": "Created", + "message": "Kreita", "description": "ex. Date this item was created" }, "datePasswordUpdated": { @@ -1398,7 +1443,7 @@ "message": "Invalid Url" }, "done": { - "message": "Done" + "message": "Preta" }, "accessibilityCookieSaved": { "message": "Accessibility cookie saved!" @@ -1407,7 +1452,7 @@ "message": "No accessibility cookie saved" }, "warning": { - "message": "WARNING", + "message": "AVERTO", "description": "WARNING (should stay in capitalized letters if the language permits)" }, "confirmVaultExport": { @@ -1435,15 +1480,15 @@ "message": "Who owns this item?" }, "strong": { - "message": "Strong", + "message": "Forta", "description": "ex. A strong password. Scale: Weak -> Good -> Strong" }, "good": { - "message": "Good", + "message": "Bona", "description": "ex. A good password. Scale: Weak -> Good -> Strong" }, "weak": { - "message": "Weak", + "message": "Malforta", "description": "ex. A weak password. Scale: Weak -> Good -> Strong" }, "weakMasterPassword": { @@ -1453,7 +1498,7 @@ "message": "The master password you have chosen is weak. You should use a strong master password (or a passphrase) to properly protect your Bitwarden account. Are you sure you want to use this master password?" }, "pin": { - "message": "PIN", + "message": "PIN-kodo", "description": "PIN code. Ex. The short code (often numeric) that you use to unlock a device." }, "unlockWithPin": { @@ -1477,21 +1522,30 @@ "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" + "message": "Malŝlosi per Touch ID" }, "additionalTouchIdSettings": { "message": "Additional Touch ID settings" }, "touchIdConsentMessage": { - "message": "unlock your vault" + "message": "malŝlosi vian trezorejon" }, "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" }, @@ -1505,7 +1559,7 @@ "message": "Lock with master password on restart" }, "deleteAccount": { - "message": "Forviŝi la konton" + "message": "Forigi la konton" }, "deleteAccountDesc": { "message": "Proceed below to delete your account and all vault data." @@ -1514,10 +1568,10 @@ "message": "Deleting your account is permanent. It cannot be undone." }, "accountDeleted": { - "message": "Konto forviŝita" + "message": "Konto forigita" }, "accountDeletedDesc": { - "message": "Via konto estas fermita, kaj ĉiuj rilataj datumoj estas forviŝitaj." + "message": "Via konto estis fermita kaj ĉiuj rilataj datumoj estis forigitaj." }, "preferences": { "message": "Preferences" @@ -1557,7 +1611,7 @@ "message": "Are you sure you want to leave? If you leave now then your current information will not be saved." }, "unsavedChangesTitle": { - "message": "Unsaved changes" + "message": "Nekonservitaj ŝanĝoj" }, "clone": { "message": "Clone" @@ -1578,11 +1632,11 @@ "message": "Set up an unlock method to change your vault timeout action." }, "lock": { - "message": "Lock", + "message": "Ŝlosi", "description": "Verb form: to make secure or inaccesible by" }, "trash": { - "message": "Trash", + "message": "Rubujo", "description": "Noun: a special folder to hold deleted items" }, "searchTrash": { @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1691,7 +1745,7 @@ "message": "By continuing, you agree to the" }, "and": { - "message": "and" + "message": "kaj" }, "acceptPolicies": { "message": "By checking this box you agree to the following:" @@ -1742,7 +1796,7 @@ "message": "By default this setting is ON. Turn OFF only if you experience graphical issues. Restart is required." }, "approve": { - "message": "Approve" + "message": "Aprobi" }, "verifyBrowserTitle": { "message": "Verify browser connection" @@ -1771,6 +1825,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 organization and choose from available collections." }, @@ -1788,10 +1848,10 @@ "description": "'Sends' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeFile": { - "message": "File" + "message": "Dosiero" }, "sendTypeText": { - "message": "Text" + "message": "Teksto" }, "searchSends": { "message": "Search Sends", @@ -1802,10 +1862,10 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "myVault": { - "message": "My vault" + "message": "Mia trezorejo" }, "text": { - "message": "Text" + "message": "Teksto" }, "deletionDate": { "message": "Deletion date" @@ -1869,7 +1929,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "newPassword": { - "message": "New password" + "message": "Nova pasvorto" }, "whatTypeOfSend": { "message": "What type of Send is this?", @@ -1886,7 +1946,7 @@ "message": "The file you want to send." }, "days": { - "message": "$DAYS$ days", + "message": "$DAYS$ tagoj", "placeholders": { "days": { "content": "$1", @@ -1895,10 +1955,10 @@ } }, "oneDay": { - "message": "1 day" + "message": "1 tago" }, "custom": { - "message": "Custom" + "message": "Propra" }, "deleteSendConfirmation": { "message": "Are you sure you want to delete this Send?", @@ -1923,7 +1983,7 @@ "message": "Copy link" }, "disabled": { - "message": "Disabled" + "message": "Malŝaltita" }, "removePassword": { "message": "Remove password" @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, @@ -1979,6 +2042,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" }, @@ -2016,10 +2082,10 @@ "message": "Resend code" }, "hours": { - "message": "Hours" + "message": "Horoj" }, "minutes": { - "message": "Minutes" + "message": "Minutoj" }, "vaultTimeoutPolicyInEffect": { "message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).", @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2076,7 +2145,7 @@ "message": "One or more organization policies prevents you from exporting your personal vault." }, "addAccount": { - "message": "Add account" + "message": "Aldoni konton" }, "removeMasterPassword": { "message": "Remove master password" @@ -2106,7 +2175,7 @@ "message": "Key connector error: make sure key connector is available and working correctly." }, "lockAllVaults": { - "message": "Lock all vaults" + "message": "Ŝlosi ĉiujn trezorejojn" }, "accountLimitReached": { "message": "No more than 5 accounts may be logged in at the same time." @@ -2166,13 +2235,13 @@ } }, "locked": { - "message": "Locked" + "message": "Ŝlosita" }, "unlocked": { - "message": "Unlocked" + "message": "Malŝlosita" }, "generator": { - "message": "Generator" + "message": "Generilo" }, "whatWouldYouLikeToGenerate": { "message": "What would you like to generate?" @@ -2203,25 +2272,25 @@ "message": "Use your domain's configured catch-all inbox." }, "random": { - "message": "Random" + "message": "Hazarda" }, "randomWord": { - "message": "Random word" + "message": "Hazarda vorto" }, "websiteName": { "message": "Website name" }, "service": { - "message": "Service" + "message": "Servo" }, "allVaults": { - "message": "All vaults" + "message": "Ĉiuj trezorejoj" }, "searchOrganization": { "message": "Search organization" }, "searchMyVault": { - "message": "Search my vault" + "message": "Traserĉi mian trezorejon" }, "forwardedEmail": { "message": "Forwarded email alias" @@ -2339,7 +2408,7 @@ "message": "API Access Token" }, "apiKey": { - "message": "API key" + "message": "API-ŝlosilo" }, "premiumSubcriptionRequired": { "message": "Premium subscription required" @@ -2354,7 +2423,7 @@ "message": "Are you sure you want to use the \"Never\" option? Setting your lock options to \"Never\" stores your vault's encryption key on your device. If you use this option you should ensure that you keep your device properly protected." }, "vault": { - "message": "Vault" + "message": "Trezorejo" }, "loginWithMasterPassword": { "message": "Log in with master password" @@ -2424,10 +2493,10 @@ "message": "Device Type" }, "ipAddress": { - "message": "IP Address" + "message": "IP-adreso" }, "time": { - "message": "Time" + "message": "Tempo" }, "confirmLogIn": { "message": "Confirm login" @@ -2497,7 +2566,7 @@ "message": "No email?" }, "goBack": { - "message": "Go back" + "message": "Reveni" }, "toEditYourEmailAddress": { "message": "to edit your email address." @@ -2563,7 +2632,7 @@ "message": "Approve with master password" }, "region": { - "message": "Region" + "message": "Regiono" }, "ssoIdentifierRequired": { "message": "Organization SSO identifier is required." @@ -2612,7 +2681,7 @@ "message": "required" }, "search": { - "message": "Search" + "message": "Serĉi" }, "inputMinLength": { "message": "Input must be at least $COUNT$ characters long.", @@ -2679,7 +2748,7 @@ } }, "selectPlaceholder": { - "message": "-- Select --" + "message": "-- Elekti --" }, "multiSelectPlaceholder": { "message": "-- Type to filter --" @@ -2694,7 +2763,7 @@ "message": "Clear all" }, "plusNMore": { - "message": "+ $QUANTITY$ more", + "message": "+ $QUANTITY$ pli", "placeholders": { "quantity": { "content": "$1", @@ -2703,7 +2772,7 @@ } }, "submenu": { - "message": "Submenu" + "message": "Submenuo" }, "toggleSideNavigation": { "message": "Toggle side navigation" @@ -2737,7 +2806,7 @@ "message": "Resolve the errors below and try again." }, "description": { - "message": "Description" + "message": "Priskribo" }, "importSuccess": { "message": "Data successfully imported" @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Celo" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2914,7 +2986,7 @@ "message": "Try again or look for an email from LastPass to verify it's you." }, "collection": { - "message": "Collection" + "message": "Kolekto" }, "lastPassYubikeyDesc": { "message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button." @@ -2958,7 +3030,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "Reveni al $NAME$", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -2968,7 +3040,7 @@ } }, "back": { - "message": "Back", + "message": "Reveni", "description": "Button text to navigate back" }, "removeItem": { @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Datumoj" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index a0fc028f830..2209d663492 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Contraseña maestra" + }, + "masterPassImportant": { + "message": "¡Tu contraseña maestra no se puede recuperar si la olvidas!" + }, + "confirmMasterPassword": { + "message": "Confirmar contraseña maestra" + }, + "masterPassHintLabel": { + "message": "Pista de la contraseña maestra" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Ajustes" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "¡Tu nueva cuenta ha sido creada! Ahora puedes acceder." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "Te hemos enviado un correo electrónico con la pista de tu contraseña maestra." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Código de verificación requerido." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Código de verificación incorrecto" }, @@ -667,17 +694,17 @@ "message": "Aplicación de autenticación" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Introduce un código generado por una aplicación de autenticación como Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP security key" + "message": "Llave de seguridad Yubico OTP" }, "yubiKeyDesc": { "message": "Usa un Yubikey para acceder a tu cuenta. Funciona con YubiKey 4, 4 Nano, 4C y dispositivos NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Introduce un código generado por Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -694,7 +721,7 @@ "message": "Correo electrónico" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Introduce el código enviado a tu dirección de correo electrónico." }, "loginUnavailable": { "message": "Entrada no disponible" @@ -777,6 +804,18 @@ "loginExpired": { "message": "Tu sesión ha expirado." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "¿Estás seguro de querer cerrar sesión?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Puedes comprar la membresía Premium en la caja fuerte web de bitwarden.com. ¿Quieres visitar el sitio web ahora?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "¡Eres un miembro Premium!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Error de actualización del token de acceso" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "Solicitar Windows Hello al iniciar" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Solicitar Touch ID al iniciar" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Su nueva contraseña maestra no cumple con los requisitos de la política." }, - "receiveMarketingEmails": { - "message": "Obtén correos electrónicos de Bitwarden para anuncios, consejos y oportunidades de investigación." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Darse de baja" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "La biometría del navegador requiere habilitar primero la biometría de escritorio en los ajustes." }, + "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": "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." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Verificación de correo electrónico requerida" }, + "emailVerifiedV2": { + "message": "Correo electrónico verificado" + }, "emailVerificationRequiredDesc": { "message": "Debes verificar tu correo electrónico para usar esta característica." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "El tiempo de espera de tu caja fuerte excede las restricciones establecidas por tu organización." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Inscripción automática" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Inicia Duo y sigue los pasos para terminar de iniciar sesión." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Contraseña de archivo no válida, por favor utiliza la contraseña que introdujiste cuando creaste el archivo de exportación." }, - "importDestination": { - "message": "Destino de importación" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Aprende acerca de tus opciones de importación" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index e799f8a2f40..76d1b4dc763 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -404,7 +404,7 @@ "message": "Pikkus" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "Parooli miinimumpikkus" }, "uppercase": { "message": "Suurtäht (A-Z)" @@ -479,7 +479,7 @@ "message": "Maksimaalne faili suurus on 500 MB." }, "encryptionKeyMigrationRequired": { - "message": "Encryption key migration required. Please login through the web vault to update your encryption key." + "message": "Krüpteerimisvõtme ühendamine nõutud. Palun logi sisse läbi veebibrauseri, et uuendada enda krüpteerimisvõtit." }, "editedFolder": { "message": "Kaust on muudetud" @@ -500,10 +500,10 @@ "message": "Konto loomine" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Määra tugev parool" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" + "message": "Lõpeta konto loomine määrates parooli" }, "logIn": { "message": "Logi sisse" @@ -527,7 +527,7 @@ "message": "Ülemparooli vihje (ei ole kohustuslik)" }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Kui sa unustad oma parooli, saad saata parooli vihje e-mailile.\n$CURRENT$/$MAXIMUM$ tähepiirang.", "placeholders": { "current": { "content": "$1", @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Ülemparool" + }, + "masterPassImportant": { + "message": "Ülemparooli ei saa taastada, kui sa selle unustama peaksid!" + }, + "confirmMasterPassword": { + "message": "Kinnita ülemparool" + }, + "masterPassHintLabel": { + "message": "Ülemparooli vihje" + }, + "joinOrganization": { + "message": "Liitu organisatsiooniga" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Lõpeta organisatsiooniga liitumine määrates ülemparool." + }, "settings": { "message": "Seaded" }, @@ -574,10 +592,10 @@ } }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Sisselogimine õnnestus" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Võid selle akna sulgeda" }, "masterPassDoesntMatch": { "message": "Ülemparoolid ei ühti." @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Sinu konto on loodud! Võid nüüd sisse logida." }, + "newAccountCreated2": { + "message": "Uus konto loodud!" + }, + "youHaveBeenLoggedIn": { + "message": "Olete sisse logitud!" + }, "masterPassSent": { "message": "Ülemparooli vihje saadeti Sinu e-postile." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Nõutav on kinnituskood." }, + "webauthnCancelOrTimeout": { + "message": "Autentimine tühistati või kestis liiga kaua aega. Palun proovi uuesti." + }, "invalidVerificationCode": { "message": "Vale kinnituskood" }, @@ -667,17 +694,17 @@ "message": "Autentimise rakendus" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Sisesta oma autentikaatori (näiteks Bitwarden Authenticator) genereeritud kood.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP security key" + "message": "Yubico OTP turvavõti" }, "yubiKeyDesc": { "message": "Kasuta kontole ligipääsemiseks YubiKey-d. See töötab YubiKey 4, 4 Nano, 4C ja NEO seadmetega." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Sisesta Duo Security genereeritud kood.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -694,7 +721,7 @@ "message": "E-post" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Sisesta oma emailile saadetud kood." }, "loginUnavailable": { "message": "Sisselogimine ei ole saadaval" @@ -715,13 +742,13 @@ "message": "Specify the base URL of your on-premise hosted bitwarden installation." }, "selfHostedBaseUrlHint": { - "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" + "message": "Sisesta enda ise-majutatud Bitwardeni serveri nimi (base URL). Näiteks: https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { - "message": "For advanced configuration, you can specify the base URL of each service independently." + "message": "Täpsemaks seadistamiseks võid määrata serveri nime (base URL) igale teenusele eraldi." }, "selfHostedEnvFormInvalid": { - "message": "You must add either the base Server URL or at least one custom environment." + "message": "Sa pead lisama serveri nime (base URL) või vähemalt ühe iseseadistatud keskkonna." }, "customEnvironment": { "message": "Kohandatud keskkond" @@ -772,11 +799,23 @@ "message": "Välja logitud" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "Kontolt edukalt välja logitud." }, "loginExpired": { "message": "Sessioon on aegunud." }, + "restartRegistration": { + "message": "Alusta registreerimist uuesti" + }, + "expiredLink": { + "message": "Aegunud link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Palun alusta registreerimist uuesti või proovi sisse logida." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Sul on juba võib-olla konto" + }, "logOutConfirmation": { "message": "Oled kindel, et soovid välja logida?" }, @@ -832,17 +871,17 @@ "message": "Muuda ülemparooli" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Jätka veebibrauseris?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Ülemparooli saab muuta Bitwardeni veebirakenduses." }, "fingerprintPhrase": { - "message": "Sõrmejälje fraas", + "message": "Unikaalne sõnajada", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." }, "yourAccountsFingerprint": { - "message": "Konto sõrmejälje fraas", + "message": "Sinu konto unikaalne sõnajada", "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": { @@ -1121,7 +1160,7 @@ "message": "1 GB ulatuses krüpteeritud salvestusruum." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Eraomanduses kaheastmelise logimise valikud, nagu näiteks YubiKey ja Duo." }, "premiumSignUpReports": { "message": "Parooli hügieen, konto seisukord ja andmelekete raportid aitavad hoidlat turvalisena hoida." @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Saad Preemium versiooni osta bitwarden.com veebihoidlas. Soovid seda kohe teha?" }, + "premiumPurchaseAlertV2": { + "message": "Sa saad hankida Preemiumi oma konto seadetes veebibrauseris." + }, "premiumCurrentMember": { "message": "Oled preemium kasutaja!" }, @@ -1243,11 +1285,14 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { - "message": "Access Token Refresh Error" + "message": "Juurdepääsukoodi Värskendamine Ebaõnnestus" }, "errorRefreshingAccessTokenDesc": { - "message": "No refresh token or API keys found. Please try logging out and logging back in." + "message": "Ei leidnud värskendamise koodi või API võtit. Palun proovi logida välja ja uuesti sisse." }, "help": { "message": "Abi" @@ -1337,7 +1382,7 @@ "description": "ex. Date this password was updated" }, "exportFrom": { - "message": "Export from" + "message": "Ekspordi asukohast" }, "exportVault": { "message": "Ekspordi hoidla" @@ -1346,31 +1391,31 @@ "message": "Failivorming" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "See eksporditav fail on parooliga kaitstud ja nõuab dekrüpteerimiseks parooli." }, "filePassword": { - "message": "File password" + "message": "Faili parool" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "Seda parooli kasutatakse selle faili eksportimiseks ja importimiseks" }, "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": "Kasuta oma konto krüpteerimise võtit, mis koosneb sinu kasutajanimest ja ülemparoolist, et krüpteerida fail ja takistada selle importimine teistesse kontodesse." }, "passwordProtected": { - "message": "Password protected" + "message": "Parooliga kaitstud" }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "Määra faili parool, et see krüpteerida ja importida teise Bitwardeni kontosse kasutates seda parooli dekrüpteerimiseks." }, "exportTypeHeading": { - "message": "Export type" + "message": "Ekspordi tüüp" }, "accountRestricted": { - "message": "Account restricted" + "message": "Kontosisene" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "\"Faili parool\" ja \"Faili parooli kinnitus\" ei kattu." }, "hCaptchaUrl": { "message": "hCaptcha Url", @@ -1469,22 +1514,28 @@ "message": "Vale PIN kood." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "Liiga palju ebaõnnestunud katseid. Login välja." }, "unlockWithWindowsHello": { "message": "Lukusta lahti Windows Helloga" }, "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" + "message": "Windows Hello lisaseaded" + }, + "unlockWithPolkit": { + "message": "Ava arvuti autentimissüsteemiga" }, "windowsHelloConsentMessage": { "message": "Kinnita Bitwardenisse sisselogimine." }, + "polkitConsentMessage": { + "message": "Autentiteeri ennast Bitwardeni avamiseks." + }, "unlockWithTouchId": { "message": "Lukusta lahti Touch ID-ga" }, "additionalTouchIdSettings": { - "message": "Additional Touch ID settings" + "message": "Muud Touch ID seaded" }, "touchIdConsentMessage": { "message": "Kinnita Bitwardenisse sisselogimine." @@ -1492,14 +1543,17 @@ "autoPromptWindowsHello": { "message": "Küsi avamisel Windows Hello tuvastust" }, + "autoPromptPolkit": { + "message": "Kasuta käivitamisel arvuti autentimissüsteemi" + }, "autoPromptTouchId": { "message": "Küsi avamisel Touch ID tuvastust" }, "requirePasswordOnStart": { - "message": "Require password or PIN on app start" + "message": "Nõua parooli või PINi rakenduse kävitumisel" }, "recommendedForSecurity": { - "message": "Recommended for security." + "message": "Soovitatud turvalisuse huvides." }, "lockWithMasterPassOnRestart": { "message": "Lukusta ülemparooliga, kui rakendus taaskäivitatakse" @@ -1575,7 +1629,7 @@ "message": "Hoidlast väljalogimine nõuab taaskordseks ligipääsuks uut autentimist." }, "unlockMethodNeededToChangeTimeoutActionDesc": { - "message": "Set up an unlock method to change your vault timeout action." + "message": "Hoidla ajalõpu muutmiseks vali esmalt lahtilukustamise meetod." }, "lock": { "message": "Lukusta", @@ -1616,15 +1670,15 @@ "message": "Määra ülemparool" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "Teie organisatsiooni seadeid värskendati, mistõttu peate määrama ülemparooli.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "Sinu organisatsioon nõuab sult ülemparooli seadistamist.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { - "message": "Verification required", + "message": "Tuvastamine vajalik", "description": "Default title for the user verification dialog." }, "currentMasterPass": { @@ -1678,20 +1732,20 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Uus ülemparool ei vasta eeskirjades väljatoodud tingimustele." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Soovin saada nõuandeid, uudiseid ja pakkumisi Bitwardenilt oma postkasti." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "Lõpeta tellimus" }, "atAnyTime": { - "message": "at any time." + "message": "iga hetk." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Jätkates nõustud sa" }, "and": { - "message": "and" + "message": "ja" }, "acceptPolicies": { "message": "Märkeruudu markeerimisel nõustud järgnevaga:" @@ -1715,10 +1769,10 @@ "message": "Brauseri integratsioon ei ole toetatud" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Brauseriga ühendamine ebaõnnestus" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Midagi läks valesti brauseriga ühendamisel." }, "browserIntegrationMasOnlyDesc": { "message": "Paraku on brauseri integratsioon hetkel toetatud ainult Mac App Store'i versioonis." @@ -1730,16 +1784,16 @@ "message": "Paraku ei ole brauseri integratsioon hetkel Linuxi versioonis toetatud." }, "enableBrowserIntegrationFingerprint": { - "message": "Nõua brauseri integratsiooni ülekinnitamist" + "message": "Nõua brauseri ühendamiseks kinnitust" }, "enableBrowserIntegrationFingerprintDesc": { - "message": "See seadistus võimaldab täiendavat kaitset, küsides brauseriga liidestamisel sõrmejälje fraasi. Sisselülitamisel nõuab see seadistus igakordset kasutaja sekkumist, kui luuakse ühendus brauseri ja töölaua rakenduse vahel." + "message": "See seadistus võimaldab täiendavat kaitset, küsides brauseriga ühendamisel teie unikaalset sõnajada. Sisselülitamisel nõuab see seadistus iga kord kasutaja kinnitust, kui luuakse ühendus brauseri ja töölaua rakenduse vahel." }, "enableHardwareAcceleration": { - "message": "Use hardware acceleration" + "message": "Kasuta riistvaralist kiirendamist" }, "enableHardwareAccelerationDesc": { - "message": "By default this setting is ON. Turn OFF only if you experience graphical issues. Restart is required." + "message": "See seade on vaikimisi SEES. Lülita see VÄLJA kui sul tekib probleeme graafikaga. Arvuti tuleb pärast seda taaskäivitada." }, "approve": { "message": "Kinnita" @@ -1748,7 +1802,7 @@ "message": "Brauseri ühendamise kinnitamine" }, "verifyBrowserDesc": { - "message": "Veendu, et kuvatav sõrmejälje fraas on identne sellega, mida kuvatakse brauseri lisas." + "message": "Veendu, et kuvatav unikaalne sõnajada on identne sellega, mida kuvatakse brauseris." }, "verifyNativeMessagingConnectionTitle": { "message": "$APPID$ soovib Bitwardeniga ühendust luua", @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "Selleks, et kasutada biomeetriat brauseris, peab selle esmalt Bitwardeni töölaua rakenduse seadetes sisse lülitama." }, + "biometricsManualSetupTitle": { + "message": "Automaatne seadistamine ei ole saadaval" + }, + "biometricsManualSetupDesc": { + "message": "Tänu sinu installimise meetodile ei õnnestunud automaatselt biomeetria sisse lülitada. Kas soovid avada juhise kuidas seda käsitsi teha?" + }, "personalOwnershipSubmitError": { "message": "Ettevõtte poliitika tõttu ei saa sa andmeid oma personaalsesse Hoidlasse salvestada. Vali Omanikuks organisatsioon ja vali mõni saadavaolevatest Kogumikest." }, @@ -1781,7 +1841,7 @@ "message": "Organisatsiooni poliitika on seadnud omaniku valikutele piirangu." }, "personalOwnershipPolicyInEffectImports": { - "message": "An organization policy has blocked importing items into your individual vault." + "message": "Kirjete importimine isiklikku hoidlasse on organisatsiooni poolt keelatud." }, "allSends": { "message": "Kõik Sendid", @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Vajalik on e-posti kinnitamine" }, + "emailVerifiedV2": { + "message": "Email kinnitatud" + }, "emailVerificationRequiredDesc": { "message": "Enne selle funktsiooni kasutamist pead oma e-posti kinnitama." }, @@ -1979,41 +2042,44 @@ "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": "Sinu organisatsioon on keelanud ära krüpteerimisvõtmete hoiustamise arvutites. Palun määra ülemparool oma hoidlale ligi pääsemiseks." + }, "tryAgain": { - "message": "Try again" + "message": "Proovi uuesti" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "Selle muudatuse jõustamiseks on vaja teid kinnitada. Jätkamiseks sisestage PIN." }, "setPin": { - "message": "Set PIN" + "message": "Määra PIN" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Kinnita biomeetriaga" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Ootan kinnitust" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "Biomeetria kasutamine ebaõnnestus." }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "Soovid kasutada teist võimalust?" }, "useMasterPassword": { - "message": "Use master password" + "message": "Kasuta ülemparooli" }, "usePin": { - "message": "Use PIN" + "message": "Kasuta PIN-koodi" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Kasuta biomeetriat" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "Sisesta oma e-posti aadressile saadetud kinnituskood." }, "resendCode": { - "message": "Resend code" + "message": "Saada kood uuesti" }, "hours": { "message": "Tundi" @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Valitud hoidla ajalõpp ei ole organisatsiooni poolt määratud reeglitega kooskõlas." }, + "inviteAccepted": { + "message": "Kutse vastu võetud" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automaatne liitumine" }, @@ -2133,7 +2202,7 @@ "message": "Vaheta kontot" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "On juba konto?" }, "options": { "message": "Valikud" @@ -2154,10 +2223,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "Ekspordin organisatsiooni hoidlat" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Ainult organisatsiooniga $ORGANIZATION$ seotud kirjed eksportidakse. Personaalsete hoidlate ja teiste organisatsioonide kirjeid ei ekspordita.", "placeholders": { "organization": { "content": "$1", @@ -2244,11 +2313,11 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "Genereeritud Bitwardeni poolt.", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "Veebisait: $WEBSITE$. Genereeritud Bitwardeni poolt.", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -2258,7 +2327,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "Vigane $SERVICENAME$ API võti", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -2268,7 +2337,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "Vigane $SERVICENAME$ API võti: $ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -2282,7 +2351,7 @@ } }, "forwarderNoAccountId": { - "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "message": "Ei õnnestunud hankida pakkuja $SERVICENAME$ maskeeritud emaili konto ID-d.", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -2292,7 +2361,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "Vigane $SERVICENAME$ domeen.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -2302,7 +2371,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "Vigane $SERVICENAME$ URL.", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -2312,7 +2381,7 @@ } }, "forwarderUnknownError": { - "message": "Unknown $SERVICENAME$ error occurred.", + "message": "Tundmatu error pakkujaga $SERVICENAME$.", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -2322,7 +2391,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Unknown forwarder: '$SERVICENAME$'.", + "message": "Tundmatu edastaja: '$SERVICENAME$'.", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -2384,16 +2453,16 @@ "message": "Logi sisse läbi teise seadme" }, "loginInitiated": { - "message": "Login initiated" + "message": "Alustan sisselogimist" }, "notificationSentDevice": { "message": "Sinu seadmesse saadeti teavitus." }, "fingerprintMatchInfo": { - "message": "Veendu, et hoidla on lahti lukustatud ja sõrmejälje fraasid seadmete vahel ühtivad." + "message": "Veendu, et sinu hoidla on avatud ja unikaalne sõnajada ühtib teise seadmega." }, "fingerprintPhraseHeader": { - "message": "Sõrmejälje fraas" + "message": "Unikaalne sõnajada" }, "needAnotherOption": { "message": "Bitwardeni rakenduse seadistuses peab olema konfigureeritud sisselogimine läbi seadme. Vajad teist valikut?" @@ -2482,25 +2551,25 @@ "message": "Sisselogimise päring on saadetud" }, "creatingAccountOn": { - "message": "Creating account on" + "message": "Loon kontot asukohas" }, "checkYourEmail": { - "message": "Check your email" + "message": "Kontrollige oma postkasti" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "Ava sulle emailiga saadetud link" }, "andContinueCreatingYourAccount": { - "message": "and continue creating your account." + "message": "ja jätka konto loomist." }, "noEmail": { - "message": "No email?" + "message": "Pole emaili?" }, "goBack": { - "message": "Go back" + "message": "Tagasi" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "et muuta oma meiliaadressi." }, "exposedMasterPassword": { "message": "Ülemparool on haavatav" @@ -2521,10 +2590,10 @@ "message": "Tähtis:" }, "accessTokenUnableToBeDecrypted": { - "message": "You have been logged out because your access token could not be decrypted. Please log in again to resolve this issue." + "message": "Sind logiti välja, sest sinu juurdepääsuvõtit (access token) ei õnnestunud dekrüpteerida. Probleemi lahendamiseks palun logige uuesti sisse." }, "refreshTokenSecureStorageRetrievalFailure": { - "message": "You have been logged out because your refresh token could not be retrieved. Please log in again to resolve this issue." + "message": "Sind logiti välja, sest sinu uuendamisvõtit (refresh token) ei õnnestunud saada. Probleemi lahendamiseks palun logige uuesti sisse." }, "masterPasswordHint": { "message": "Ülemparooli ei saa taastada, kui sa selle unustama peaksid!" @@ -2539,83 +2608,83 @@ } }, "windowsBiometricUpdateWarning": { - "message": "Bitwarden recommends updating your biometric settings to require your master password (or PIN) on the first unlock. Would you like to update your settings now?" + "message": "Bitwarden soovitab muuta oma biomeetria seadeid, et nõuda esimesel sisselogimisel ülemparooli (või PINi). Kas soovid uuendada oma seadeid kohe?" }, "windowsBiometricUpdateWarningTitle": { - "message": "Recommended Settings Update" + "message": "Soovitatud Muudatus Seadetes" }, "deviceApprovalRequired": { - "message": "Device approval required. Select an approval option below:" + "message": "Seadme kinnitamine on nõutud. Palun vali kuidas soovid seda teha:" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Hoia see seade meeles" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "Lülita see välja, kui oled avalikus seadmes" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Kinnita teises seadmes" }, "requestAdminApproval": { - "message": "Request admin approval" + "message": "Küsi administraatori nõusolekut" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "Kinnita ülemparooliga" }, "region": { - "message": "Region" + "message": "Piirkond" }, "ssoIdentifierRequired": { - "message": "Organization SSO identifier is required." + "message": "Organisatsiooni SSO identifikaator on nõutud." }, "eu": { - "message": "EU", + "message": "EL", "description": "European Union" }, "loggingInOn": { - "message": "Logging in on" + "message": "Login sisse aadressil" }, "selfHostedServer": { - "message": "self-hosted" + "message": "ise majutatud" }, "accessDenied": { - "message": "Access denied. You do not have permission to view this page." + "message": "Ligipääs keelatud. Sul pole lubatud seda lehekülge vaadata." }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Konto edukalt loodud!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "Taotlus administraatorile saadetud" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "Sinu taotlus saadeti administraatorile." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Kinnitamise järel saad selle kohta teavituse." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Ei õnnestu sisse logida?" }, "loginApproved": { - "message": "Login approved" + "message": "Sisselogimine kinnitatud" }, "userEmailMissing": { - "message": "User email missing" + "message": "Kasutaja email puudub" }, "deviceTrusted": { - "message": "Device trusted" + "message": "Usaldusväärne seade" }, "inputRequired": { - "message": "Input is required." + "message": "Sisend on nõutud." }, "required": { - "message": "required" + "message": "nõutud" }, "search": { - "message": "Search" + "message": "Otsi" }, "inputMinLength": { - "message": "Input must be at least $COUNT$ characters long.", + "message": "Sisend peab olema vähemalt $COUNT$ tähemärki pikk.", "placeholders": { "count": { "content": "$1", @@ -2624,7 +2693,7 @@ } }, "inputMaxLength": { - "message": "Input must not exceed $COUNT$ characters in length.", + "message": "Sisend ei tohi olla üle $COUNT$ tähemärgi pikkune.", "placeholders": { "count": { "content": "$1", @@ -2633,7 +2702,7 @@ } }, "inputForbiddenCharacters": { - "message": "The following characters are not allowed: $CHARACTERS$", + "message": "Järgnevad märgid pole lubatud: $CHARACTERS$", "placeholders": { "characters": { "content": "$1", @@ -2642,7 +2711,7 @@ } }, "inputMinValue": { - "message": "Input value must be at least $MIN$.", + "message": "Selle väärtus peab olema vähemalt $MIN$.", "placeholders": { "min": { "content": "$1", @@ -2651,7 +2720,7 @@ } }, "inputMaxValue": { - "message": "Input value must not exceed $MAX$.", + "message": "Selle väärtus ei tohi ületada $MAX$.", "placeholders": { "max": { "content": "$1", @@ -2660,17 +2729,17 @@ } }, "multipleInputEmails": { - "message": "1 or more emails are invalid" + "message": "Üks või rohkem emaili on vigased" }, "inputTrimValidator": { - "message": "Input must not contain only whitespace.", + "message": "Sisend ei tohi koosneda ainult tühikutest.", "description": "Notification to inform the user that a form's input can't contain only whitespace." }, "inputEmail": { - "message": "Input is not an email address." + "message": "See pole e-posti aadress." }, "fieldsNeedAttention": { - "message": "$COUNT$ field(s) above need your attention.", + "message": "$COUNT$ välja nõuab tähelepanu.", "placeholders": { "count": { "content": "$1", @@ -2679,22 +2748,22 @@ } }, "selectPlaceholder": { - "message": "-- Select --" + "message": "-- Vali --" }, "multiSelectPlaceholder": { - "message": "-- Type to filter --" + "message": "-- Filtreerimiseks trüki siia --" }, "multiSelectLoading": { - "message": "Retrieving options..." + "message": "Valikute hankimine..." }, "multiSelectNotFound": { - "message": "No items found" + "message": "Ühtki kirjet ei leitud" }, "multiSelectClearAll": { - "message": "Clear all" + "message": "Tühjenda kõik" }, "plusNMore": { - "message": "+ $QUANTITY$ more", + "message": "+ $QUANTITY$ veel", "placeholders": { "quantity": { "content": "$1", @@ -2703,47 +2772,47 @@ } }, "submenu": { - "message": "Submenu" + "message": "Alammenüü" }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "Lülita sisse külgpaneel" }, "skipToContent": { - "message": "Skip to content" + "message": "Sisu juurde" }, "typePasskey": { - "message": "Passkey" + "message": "Pääsuvõti" }, "passkeyNotCopied": { - "message": "Passkey will not be copied" + "message": "Pääsuvõtit ei kopeerita" }, "passkeyNotCopiedAlert": { - "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" + "message": "Pääsukoodi ei kopeerita kloonitud kirjele. Oled kindel, et soovid jätkata?" }, "aliasDomain": { - "message": "Alias domain" + "message": "Varidomeen" }, "importData": { - "message": "Import data", + "message": "Impordi andmed", "description": "Used for the desktop menu item and the header of the import dialog" }, "importError": { - "message": "Import error" + "message": "Tõrge importimisel" }, "importErrorDesc": { - "message": "There was a problem with the data you tried to import. Please resolve the errors listed below in your source file and try again." + "message": "Andmete importimisel ilmnes tõrge. Paranda originaalfailis olevad vead (kuvatud all) ning proovi uuesti." }, "resolveTheErrorsBelowAndTryAgain": { - "message": "Resolve the errors below and try again." + "message": "Lahenda allolevad probleemid ja proovi uuesti." }, "description": { - "message": "Description" + "message": "Kirjeldus" }, "importSuccess": { - "message": "Data successfully imported" + "message": "Andmed edukalt imporditud" }, "importSuccessNumberOfItems": { - "message": "A total of $AMOUNT$ items were imported.", + "message": "Kokku imporditi $AMOUNT$ kirjet.", "placeholders": { "amount": { "content": "$1", @@ -2752,10 +2821,10 @@ } }, "total": { - "message": "Total" + "message": "Kokku" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "Impordid andmeid organisatsiooni $ORGANIZATION$. Neid andmeid võidakse jagada teiste organisatsiooni liikmetega. Soovid jätkata?", "placeholders": { "organization": { "content": "$1", @@ -2763,41 +2832,44 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Duo teenustega ühendamine ebaõnnestus. Kasuta teist kahe-astmelise sisselogimise meetodit või kontakteeru Duoga." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "Käivita Duo ja järgi juhiseid, et lõpetada sisselogimine." }, "duoRequiredByOrgForAccount": { - "message": "Duo two-step login is required for your account." + "message": "Duo kahe-astmeline sisselogimine on sinu kontol nõutud." }, "launchDuo": { - "message": "Launch Duo in Browser" + "message": "Käivita Duo brauseris" }, "importFormatError": { - "message": "Data is not formatted correctly. Please check your import file and try again." + "message": "Andmed ei ole korrektsed. Palun kontrollige imporditavat faili ja proovige uuesti." }, "importNothingError": { - "message": "Nothing was imported." + "message": "Ei imporditud midagi." }, "importEncKeyError": { - "message": "Error decrypting the exported file. Your encryption key does not match the encryption key used export the data." + "message": "Eksporditud faili dekrüpteerimine nurjus. Sinu krüpteerimisvõti ei ühti selle võtmega, mida kasutati andmete eksportimisel." }, "invalidFilePassword": { - "message": "Invalid file password, please use the password you entered when you created the export file." + "message": "Vale parool, palun kasuta seda parooli mille sisestasid eksportfaili loomisel." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Sihtkoht" }, "learnAboutImportOptions": { - "message": "Learn about your import options" + "message": "Lisainfo importimise valikute kohta" }, "selectImportFolder": { - "message": "Select a folder" + "message": "Vali kaust" }, "selectImportCollection": { - "message": "Select a collection" + "message": "Vali kogumik" }, "importTargetHint": { - "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", + "message": "Tee siin valik, kui soovid, et imporditud faili sisu liigutatakse asukohta $DESTINATION$", "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", "placeholders": { "destination": { @@ -2807,25 +2879,25 @@ } }, "importUnassignedItemsError": { - "message": "File contains unassigned items." + "message": "Fail sisaldab määramata kirjeid." }, "selectFormat": { - "message": "Select the format of the import file" + "message": "Vali imporditava faili formaat" }, "selectImportFile": { - "message": "Select the import file" + "message": "Vali imporditav fail" }, "chooseFile": { - "message": "Choose File" + "message": "Vali fail" }, "noFileChosen": { - "message": "No file chosen" + "message": "Ühtegi faili pole valitud" }, "orCopyPasteFileContents": { - "message": "or copy/paste the import file contents" + "message": "või kopeeri/kleebi imporditava faili sisu" }, "instructionsFor": { - "message": "$NAME$ Instructions", + "message": "$NAME$ Kasutusjuhend", "description": "The title for the import tool instructions.", "placeholders": { "name": { @@ -2835,120 +2907,120 @@ } }, "confirmVaultImport": { - "message": "Confirm vault import" + "message": "Kinnita hoidla importimine" }, "confirmVaultImportDesc": { - "message": "This file is password-protected. Please enter the file password to import data." + "message": "Fail on parooliga kaitstud. Palun sisesta faili importimiseks selle parool." }, "confirmFilePassword": { - "message": "Confirm file password" + "message": "Kinnita faili parool" }, "exportSuccess": { - "message": "Vault data exported" + "message": "Hoidla sisu edukalt eksporditud" }, "multifactorAuthenticationCancelled": { - "message": "Multifactor authentication cancelled" + "message": "Mitmeastmeline autentiteerimine tühistatud" }, "noLastPassDataFound": { - "message": "No LastPass data found" + "message": "Ei leidnud LastPassi andmeid" }, "incorrectUsernameOrPassword": { - "message": "Incorrect username or password" + "message": "Vale kasutajanimi või parool" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "Vale parool" }, "incorrectCode": { - "message": "Incorrect code" + "message": "Vale kood" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "Vale PIN-kood" }, "multifactorAuthenticationFailed": { - "message": "Multifactor authentication failed" + "message": "Mitmeastmeline autentiteerimine ebaõnnestus" }, "includeSharedFolders": { - "message": "Include shared folders" + "message": "Lisa ka jagatud kaustad" }, "lastPassEmail": { - "message": "LastPass Email" + "message": "LastPassi Email" }, "importingYourAccount": { - "message": "Importing your account..." + "message": "Impordin sinu kontot..." }, "lastPassMFARequired": { - "message": "LastPass multifactor authentication required" + "message": "LastPassi mitmeastmeline autentiteerimine nõutud" }, "lastPassMFADesc": { - "message": "Enter your one-time passcode from your authentication app" + "message": "Sisesta oma ühekordne kood autentiteerimise rakendusest" }, "lastPassOOBDesc": { - "message": "Approve the login request in your authentication app or enter a one-time passcode." + "message": "Kinnita sisselogimistaotlus oma autentiteerimisrakenduses või sisesta ühekordne kood." }, "passcode": { - "message": "Passcode" + "message": "Pääsukood" }, "lastPassMasterPassword": { - "message": "LastPass master password" + "message": "LastPassi ülemparool" }, "lastPassAuthRequired": { - "message": "LastPass authentication required" + "message": "LastPassi autentiteerimine nõutud" }, "awaitingSSO": { - "message": "Awaiting SSO authentication" + "message": "Ootan SSO autentiteerimise kinnitamist" }, "awaitingSSODesc": { - "message": "Please continue to log in using your company credentials." + "message": "Sisselogimiseks palun jätka kasutades oma firma andmeid." }, "seeDetailedInstructions": { - "message": "See detailed instructions on our help site at", + "message": "Vaata täpsemaid juhiseid meie veebilehel", "description": "This is followed a by a hyperlink to the help website." }, "importDirectlyFromLastPass": { - "message": "Import directly from LastPass" + "message": "Impordi otse LastPassist" }, "importFromCSV": { - "message": "Import from CSV" + "message": "Impordi CSV-st" }, "lastPassTryAgainCheckEmail": { - "message": "Try again or look for an email from LastPass to verify it's you." + "message": "Proovi uuesti või kinnita sulle LastPassilt saadetud kirjas, et see oled sina." }, "collection": { - "message": "Collection" + "message": "Kogumik" }, "lastPassYubikeyDesc": { - "message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button." + "message": "Sisesta sinu LastPassi kontoga seotud YubiKey oma arvuti USB porti ja vajuta nuppu selle peal." }, "commonImportFormats": { - "message": "Common formats", + "message": "Tüüpilised meetodid", "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Tehtud" }, "troubleshooting": { - "message": "Troubleshooting" + "message": "Tõrkeotsing" }, "disableHardwareAccelerationRestart": { - "message": "Disable hardware acceleration and restart" + "message": "Lülita riistavara kiirendus välja ja tee taaskäivitus" }, "enableHardwareAccelerationRestart": { - "message": "Enable hardware acceleration and restart" + "message": "Lülita riistavara kiirendus sisse ja tee taaskäivitus" }, "removePasskey": { - "message": "Remove passkey" + "message": "Eemalda pääsuvõti" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Pääsuvõti eemaldatud" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Sihtkogumikku määramine ebaõnnestus." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Sihtkausta määramine ebaõnnestus." }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "Vaata kirjeid asukohas $NAME$", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -2958,7 +3030,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "Tagasi asukohta $NAME$", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -2968,11 +3040,11 @@ } }, "back": { - "message": "Back", + "message": "Tagasi", "description": "Button text to navigate back" }, "removeItem": { - "message": "Remove $NAME$", + "message": "Eemalda $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Andmed" + }, + "fileSends": { + "message": "Kõik Send Failid" + }, + "textSends": { + "message": "Kõik Tekst-Sendid" + }, + "ssoError": { + "message": "SSO-ga sisselogimiseks ei leitud ühtegi vaba porti." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index e7347a53cf5..612db0c7f34 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Ezarpenak" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Zure kontua egina dago. Orain saioa has dezakezu." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "Mezu elektroniko bat bidali dizugu zure pasahitz nagusiaren pistarekin." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Egiaztatze-kodea behar da." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Egiaztatze-kodea ez da baliozkoa" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Saioa amaitu da." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Ziur zaude saioa itxi nahi duzula?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Zure premium bazkidetza bitwarden.com webguneko kutxa gotorrean ordaindu dezakezu. Orain bisitatu nahi duzu webgunea?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Premium bazkide zara!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "Eskatu Windows Hello abiaraztean" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Eskatu Touch ID abiaraztean" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Zure pasahitz nagusi berriak ez ditu baldintzak betetzen." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "Nabigatzailearen biometriak lehenik mahaigainaren biometria gaitzeko eskatzen du." }, + "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": "Erakundeko politika bat dela eta, ezin dituzu elementuak zure kutxa gotor pertsonalean gorde. Aldatu jabe aukera erakunde aukera batera, eta aukeratu bilduma erabilgarrien artean." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Emailaren egiaztapena behar da" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Emaila egiaztatu behar duzu funtzio hau erabiltzeko." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Zure kutxa gotorreko itxaronaldiak, zure erakundeak ezarritako murrizpenak gainditzen ditu." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Izen-emate automatikoa" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index aac7d569cb1..1d6d82185d7 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "تنظیمات" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "حساب کاربری جدید شما ساخته شد! حالا می‌توانید وارد شوید." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "ما یک ایمیل همراه با یادآور کلمه عبور اصلی برایتان ارسال کردیم." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "کد تأیید مورد نیاز است." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "کد تأیید نامعتبر است" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "نشست ورود شما منقضی شده است." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "آیا مطمئنید که می‌خواهید خارج شوید؟" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "شما می‌توانید عضویت پرمیوم را از گاوصندوق وب bitwarden.com خریداری کنید. مایلید اکنون از وب‌سایت بازید کنید؟" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "شما یک عضو پرمیوم هستید!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,15 @@ "additionalWindowsHelloSettings": { "message": "تنظیمات اضافی Windows Hello" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "تأیید برای Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "باز کردن با Touch ID" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "درخواست Windows Hello در هنگام راه اندازی" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "درخواست Touch ID در هنگام راه اندازی" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "کلمه عبور اصلی جدید شما از شرایط سیاست پیروی نمی‌کند." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "بیومتریک مرورگر ابتدا نیاز به فعالسازی بیومتریک دسکتاپ در تنظیمات دارد." }, + "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": "به دلیل سیاست پرمیوم، برای ذخیره موارد در گاوصندوق شخصی خود محدود شده اید. گزینه مالکیت را به یک سازمان تغییر دهید و مجموعه های موجود را انتخاب کنید." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "تأیید ایمیل لازم است" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "برای استفاده از این ویژگی باید ایمیل خود را تأیید کنید." }, @@ -1979,6 +2042,9 @@ "updateWeakMasterPasswordWarning": { "message": "کلمه عبور اصلی شما با یک یا چند سیاست سازمان‌تان مطابقت ندارد. برای دسترسی به گاوصندوق، باید همین حالا کلمه عبور اصلی خود را به‌روز کنید. در صورت ادامه، شما از نشست فعلی خود خارج می‌شوید و باید دوباره وارد سیستم شوید. نشست فعال در دستگاه های دیگر ممکن است تا یک ساعت همچنان فعال باقی بمانند." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "دوباره سعی کنید" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "مهلت زمانی شما بیش از محدودیت های تعیین شده توسط سازمانتان است." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "ثبت نام خودکار" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "مقصد برون ریزی" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index f42aa7d127a..adc8c092334 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Pääsalasana" + }, + "masterPassImportant": { + "message": "Pääsalasanasi palauttaminen ei ole mahdollista, jos unohdat sen!" + }, + "confirmMasterPassword": { + "message": "Vahvista pääsalasana" + }, + "masterPassHintLabel": { + "message": "Pääsalasanan vihje" + }, + "joinOrganization": { + "message": "Liity organisaatioon" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Viimeistele organisaatioon liittyminen asettamalla pääsalasana." + }, "settings": { "message": "Asetukset" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Uusi käyttäjätilisi on luotu! Voit nyt kirjautua sisään." }, + "newAccountCreated2": { + "message": "Uusi tilisi on luotu!" + }, + "youHaveBeenLoggedIn": { + "message": "Sinut on kirjattu sisään!" + }, "masterPassSent": { "message": "Lähetimme pääsalasanasi vihjeen sinulle sähköpostitse." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Todennuskoodi vaaditaan." }, + "webauthnCancelOrTimeout": { + "message": "Tunnistautuminen peruttiin tai se kesti liian kauan. Yritä uudelleen." + }, "invalidVerificationCode": { "message": "Virheellinen todennuskoodi" }, @@ -622,10 +649,10 @@ "message": "Jatka" }, "enterVerificationCodeApp": { - "message": "Syötä 6-numeroinen todennuskoodi todennussovelluksestasi." + "message": "Syötä todennussovelluksesi näyttämä kuusinumeroinen todennuskoodi." }, "enterVerificationCodeEmail": { - "message": "Syötä 6-numeroinen todennuskoodi, joka lähetettiin sähköpostitse osoitteeseen $EMAIL$.", + "message": "Syötä osoitteeseen $EMAIL$ lähetetty kuusinumeroinen todennuskoodi.", "placeholders": { "email": { "content": "$1", @@ -649,13 +676,13 @@ "message": "Lähetä todennuskoodi sähköpostitse uudelleen" }, "useAnotherTwoStepMethod": { - "message": "Käytä eri kaksivaiheisen kirjautumisen todennusmenetelmää" + "message": "Käytä vaihtoehtoista todennustapaa" }, "insertYubiKey": { "message": "Kytke YubiKey-todennuslaitteesi tietokoneen USB-porttiin ja paina sen painiketta." }, "insertU2f": { - "message": "Kytke todennuslaitteesi tietokoneen USB-porttiin ja jos laitteessa on painike, paina sitä." + "message": "Kytke suojausavaimesi tietokoneen USB-porttiin ja jos laitteessa on painike, paina sitä." }, "recoveryCodeDesc": { "message": "Etkö pysty käyttämään kaksivaiheisen kirjautumisen todentajiasi? Poista kaikki tilillesi määritetyt todentajat käytöstä palautuskoodillasi." @@ -671,7 +698,7 @@ "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP -todennuslaite" + "message": "Yubico OTP -suojausavain" }, "yubiKeyDesc": { "message": "Käytä YubiKey-todennuslaitetta tilisi avaukseen. Toimii YubiKey 4, 4 Nano, 4C sekä NEO -laitteiden kanssa." @@ -681,14 +708,14 @@ "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { - "message": "Vahvista organisaatiollesi Duo Securityn avulla käyttäen Duo Mobile ‑sovellusta, tekstiviestiä, puhelua tai U2F-todennuslaitetta.", + "message": "Vahvista organisaatiollesi Duo Securityn avulla käyttäen Duo Mobile ‑sovellusta, tekstiviestiä, puhelua tai U2F-suojausavainta.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, "webAuthnDesc": { - "message": "Avaa tilisi millä tahansa WebAuthn‑yhteensopivalla todennuslaitteella." + "message": "Avaa tilisi millä tahansa WebAuthn‑yhteensopivalla suojausavaimella." }, "emailTitle": { "message": "Sähköposti" @@ -777,6 +804,18 @@ "loginExpired": { "message": "Kirjautumisistuntosi on erääntynyt." }, + "restartRegistration": { + "message": "Aloita rekisteröityminen alusta" + }, + "expiredLink": { + "message": "Vanhentunut linkki" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Aloita rekisteröityminen alusta tai yritä kirjautua sisään uudelleen." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Sinulla saattaa jo olla tili" + }, "logOutConfirmation": { "message": "Haluatko varmasti kirjautua ulos?" }, @@ -883,7 +922,7 @@ "message": "Virheellinen pääsalasana" }, "twoStepLoginConfirmation": { - "message": "Kaksivaiheinen kirjautuminen parantaa tilisi suojausta vaatimalla kirjautumisen vahvistuksen salasanan lisäksi todennuslaitteen, ‑sovelluksen, tekstiviestin, puhelun tai sähköpostin avulla. Voit ottaa kaksivaiheisen kirjautumisen käyttöön bitwarden.com‑verkkoholvissa. Haluatko avata sen nyt?" + "message": "Kaksivaiheinen kirjautuminen parantaa tilisi suojausta vaatimalla kirjautumisen vahvistuksen salasanan lisäksi suojausavaimen, todennussovelluksen, tekstiviestin, puhelun tai sähköpostin avulla. Voit ottaa kaksivaiheisen kirjautumisen käyttöön bitwarden.com‑verkkoholvissa. Haluatko avata sen nyt?" }, "twoStepLogin": { "message": "Kaksivaiheinen kirjautuminen" @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Voit ostaa Premium-jäsenyyden bitwarden.com-verkkoholvista. Haluatko käydä sivustolla nyt?" }, + "premiumPurchaseAlertV2": { + "message": "Voit ostaa Premiumin tiliasetuksistasi Bitwardenin verkkosovelluksen kautta." + }, "premiumCurrentMember": { "message": "Olet Premium-jäsen!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Käyttötunnisteen päivitysvirhe" }, @@ -1477,9 +1522,15 @@ "additionalWindowsHelloSettings": { "message": "Windows Hello -lisäasetukset." }, + "unlockWithPolkit": { + "message": "Avaa järjestelmän tunnistautumisella" + }, "windowsHelloConsentMessage": { "message": "Vahvista Bitwarden." }, + "polkitConsentMessage": { + "message": "Avaa Bitwardenin lukitus tunnistautumalla." + }, "unlockWithTouchId": { "message": "Avaa Touch ID:llä" }, @@ -1492,6 +1543,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ä" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Uusi pääsalasanasi ei täytä käytännön määrittämiä vaatimuksia." }, - "receiveMarketingEmails": { - "message": "Vastaanota Bitwardenilta uutiskirjeitä julkaisuista, tukiresursseista ja tutkimusmahdollisuuksista." + "receiveMarketingEmailsV2": { + "message": "Vastaanota Bitwardenilta postilaatikkoosi vinkkejä, uutisia ja tutkimusmahdollisuuksia." }, "unsubscribe": { "message": "Lopeta tilaus" @@ -1771,6 +1825,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": "Asennustavasta johtuen Biometriatukea ei voitu ottaa käyttöön automaattisesti. Haluatko avata ohjeen tämän manuaaliseen määritykseen?" + }, "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." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Sähköpostiosoite on vahvistettava" }, + "emailVerifiedV2": { + "message": "Sähköpostiosoite on vahvistettu" + }, "emailVerificationRequiredDesc": { "message": "Sinun on vahvistettava sähköpostiosoitteesi käyttääksesi tätä ominaisuutta." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Holvisi aikakatkaisu ylittää organisaatiosi asettamat rajoitukset." }, + "inviteAccepted": { + "message": "Kutsu hyväksyttiin" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automaattinen liitos" }, @@ -2390,7 +2459,7 @@ "message": "Laitteellesi on lähetetty ilmoitus." }, "fingerprintMatchInfo": { - "message": "Varmista, että holvisi on avattu ja tunnistelauseke täsmää toisella laitteella." + "message": "Varmista, että vahvistavan laitteen holvi on avattu ja että se näyttää saman tunnistelausekkeen." }, "fingerprintPhraseHeader": { "message": "Tunnistelauseke" @@ -2712,13 +2781,13 @@ "message": "Siirry sisältöön" }, "typePasskey": { - "message": "Suojausavain" + "message": "Pääsyavain" }, "passkeyNotCopied": { - "message": "Suojausavainta ei kopioida" + "message": "Pääsyavainta ei kopioida" }, "passkeyNotCopiedAlert": { - "message": "Suojausavain ei kopioidu kloonattuun kohteeseen. Haluatko jatkaa kloonausta?" + "message": "Pääsyavain ei kopioidu kloonattuun kohteeseen. Haluatko jatkaa kloonausta?" }, "aliasDomain": { "message": "Aliaksen verkkotunnus" @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Virhe yhdistettäessä Duo-palveluun. Käytä vaihtoehtoista todennustapaa tai ole yhteydessä Duon asiakaspalveluun saadaksesi apua." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Avaa Duo ja viimeistele kirjautuminen seuraamalla ohjeita." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Tiedoston salasana on virheellinen. Käytä vientitiedoston luonnin yhteydessä syötettyä salasanaa." }, - "importDestination": { - "message": "Tuontikohde" + "destination": { + "message": "Määränpää" }, "learnAboutImportOptions": { "message": "Lue lisää tuontivaihtoehdoista" @@ -2936,10 +3008,10 @@ "message": "Ota laitteistokiihdytys käyttöön ja käynnistä sovellus uudelleen" }, "removePasskey": { - "message": "Poista suojausavain" + "message": "Poista pääsyavain" }, "passkeyRemoved": { - "message": "Suojausavain poistettiin" + "message": "Pääsyavain poistettiin" }, "errorAssigningTargetCollection": { "message": "Virhe määritettäessä kohdekokoelmaa." @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Tiedot" + }, + "fileSends": { + "message": "Tiedosto-Sendit" + }, + "textSends": { + "message": "Teksti-Sendit" + }, + "ssoError": { + "message": "Kertakirjautumiselle ei löytynyt vapaita portteja." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 8eb371f8dcb..28c5673877f 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Mga Preperensya" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Nalikha na ang iyong bagong account! Maaari ka nang mag-log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "Pinadala na namin sa iyo ang email na may hint ng master password mo." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Kailangan ang verification code." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Maling verification code" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Nag-expire na ang iyong session sa login." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Sigurado ka bang gusto mong mag-log out?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Maaari kang bumili ng premium membership sa bitwarden.com web vault. Gusto mo bang bisitahin ang website ngayon?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Ikaw ay isang premium na miyembro!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Ang iyong bagong master password ay hindi nakakatugon sa mga kinakailangan sa patakaran." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "Ang biometrics ng browser ay nangangailangan ng desktop biometrics na mai set up muna sa mga setting." }, + "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": "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." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Kailangan ang pag verify ng email" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Kailangan mong i verify ang iyong email upang magamit ang tampok na ito." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Ang iyong vault timeout ay lumalampas sa mga restriksiyon na itinakda ng iyong organisasyon." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Awtomatikong pagpapatala" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 817f200ce1f..fe170607562 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Mot de passe principal" + }, + "masterPassImportant": { + "message": "Votre mot de passe principal ne peut pas être récupéré si vous l'oubliez !" + }, + "confirmMasterPassword": { + "message": "Confirmer le mot de passe principal" + }, + "masterPassHintLabel": { + "message": "Indice du mot de passe principal" + }, + "joinOrganization": { + "message": "Rejoindre l'organisation" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Terminez votre adhésion à cette organisation en définissant un mot de passe principal." + }, "settings": { "message": "Paramètres" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Votre nouveau compte a été créé ! Vous pouvez maintenant vous authentifier." }, + "newAccountCreated2": { + "message": "Votre nouveau compte a été créé !" + }, + "youHaveBeenLoggedIn": { + "message": "Vous avez été connecté !" + }, "masterPassSent": { "message": "Nous vous avons envoyé un courriel avec votre indice de mot de passe principal." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Le code de vérification est requis." }, + "webauthnCancelOrTimeout": { + "message": "L'authentification a été annulée ou a pris trop de temps. Veuillez réessayer." + }, "invalidVerificationCode": { "message": "Code de vérification invalide" }, @@ -667,17 +694,17 @@ "message": "Application d'authentification" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Entrez un code généré par une application d'authentification comme Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP security key" + "message": "Clé de sécurité Yubico OTP" }, "yubiKeyDesc": { "message": "Utiliser une YubiKey pour accéder à votre compte. Fonctionne avec les appareils YubiKey 4, 4 Nano, 4C et NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Entrez un code généré par Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -694,7 +721,7 @@ "message": "Courriel" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Entrez le code envoyé par courriel." }, "loginUnavailable": { "message": "Identifiant non disponible" @@ -777,6 +804,18 @@ "loginExpired": { "message": "Votre session a expiré." }, + "restartRegistration": { + "message": "Redémarrer l'inscription" + }, + "expiredLink": { + "message": "Lien expiré" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Veuillez redémarrer votre inscription ou essayez de vous connecter." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Vous avez peut-être déjà un compte" + }, "logOutConfirmation": { "message": "Êtes-vous sûr de vouloir vous déconnecter ?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Vous pouvez acheter une adhésion Premium sur le coffre web de bitwarden.com. Voulez-vous visiter le site web maintenant ?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Vous êtes un membre Premium !" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copié avec succès" + }, "errorRefreshingAccessToken": { "message": "Erreur d'actualisation du jeton d'accès" }, @@ -1477,9 +1522,15 @@ "additionalWindowsHelloSettings": { "message": "Paramètres supplémentaires de Windows Hello" }, + "unlockWithPolkit": { + "message": "Déverrouiller avec l'authentification du système" + }, "windowsHelloConsentMessage": { "message": "Vérifier pour Bitwarden." }, + "polkitConsentMessage": { + "message": "S'authentifier pour déverrouiller Bitwarden." + }, "unlockWithTouchId": { "message": "Déverrouiller avec Touch ID" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "Demander à Windows Hello au démarrage" }, + "autoPromptPolkit": { + "message": "Demander l'authentification du système au lancement" + }, "autoPromptTouchId": { "message": "Demander Touch ID au démarrage de l'application" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Votre nouveau mot de passe principal ne répond pas aux exigences de politique de sécurité." }, - "receiveMarketingEmails": { - "message": "Recevez des courriels de Bitwarden pour des annonces, des conseils et des opportunités de recherche." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Se désabonner" @@ -1771,6 +1825,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": "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": "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." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Vérification de courriel requise" }, + "emailVerifiedV2": { + "message": "Courriel vérifié" + }, "emailVerificationRequiredDesc": { "message": "Vous devez vérifier votre courriel pour utiliser cette fonctionnalité." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Le délai d'expiration de votre coffre dépasse les restrictions définies par votre organisation." }, + "inviteAccepted": { + "message": "Invitation acceptée" + }, "resetPasswordPolicyAutoEnroll": { "message": "Inscription automatique" }, @@ -2157,7 +2226,7 @@ "message": "Export du coffre-fort de l'organisation" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Seul le coffre-fort de l'organisation associé à $ORGANIZATION$ sera exporté. Les coffres individuels ou d'autres organisations ne seront pas inclus.", "placeholders": { "organization": { "content": "$1", @@ -2230,7 +2299,7 @@ "message": "Générer un alias de courriel avec un service de transfert externe." }, "forwarderError": { - "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "message": "Erreur de $SERVICENAME$ : $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", "placeholders": { "servicename": { @@ -2258,7 +2327,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "Jeton d'API de $SERVICENAME$ non valide", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -2268,7 +2337,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "Jeton d'API de $SERVICENAME$ non valide : $ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -2282,7 +2351,7 @@ } }, "forwarderNoAccountId": { - "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "message": "Impossible d'obtenir l'ID de compte de messagerie masqué de $SERVICENAME$.", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -2302,7 +2371,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "URL de $SERVICENAME$ non valide.", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Lancez Duo et suivez les étapes pour terminer la connexion." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Mot de passe du fichier incorrect, veuillez utiliser le mot de passe saisi lors de l'exportation du fichier." }, - "importDestination": { - "message": "Destination de l'import" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "En savoir plus sur vos options d'importation" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Données" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "Fichier enregistré sur l'appareil. Gérez à partir des téléchargements de votre appareil." } } diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 0887d769823..9194fd7c22a 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Settings" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Your login session has expired." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a premium member!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,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 organization and choose from available collections." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 8d82180f1de..886755f53cd 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "הגדרות" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "החשבון החדש שלך נוצר בהצלחה! כעת ניתן להתחבר למערכת." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "שלחנו לך אימייל עם רמז לסיסמה הראשית." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "נדרש קוד אימות." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "קוד אימות שגוי" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "תוקף החיבור שלך הסתיים." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "האם אתה בטוח שברצונך להתנתק?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "באפשרותך לרכוש מנוי פרימיום בכספת באתר bitwarden.com. האם ברצונך לפתוח את האתר כעת?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "אתה מנוי פרימיום!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,15 @@ "additionalWindowsHelloSettings": { "message": "הגדרות נוספות של Windows Hello" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "אימות עבור Bitwarden." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "שחרור נעילה עם Touch ID" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "הצג את Windows Hello בפתיחת האפליקציה" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "הצג בקשה של Touch ID בפתיחת האפליקציה" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "הסיסמה הראשית החדשה השלך לא עומדת בדרישות המדיניות." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "בכדי להשתמש באמצעי אימות ביומטריים בדפדפן, אפשר תכונה זו באפליקציה בשולחן העבודה." }, + "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": "בשל מדיניות ארגונית, אתה מוגבל לשמירת פריטים בכספת האישית שלך. שנה את ההגדרות בעלות החשבון לחשבון ארגוני ובחר מתוך האוספים הזמינים." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "נדרש כתובת אימייל לאימות" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "נדרש אישור אימות בדוא\"ל כדי לאפשר שימוש בתכונה זו." }, @@ -1979,6 +2042,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": "לנסות שוב" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "הזמן הקצוב לכספת שלך חורג מהמגבלות שנקבעו על ידי הארגון שלך." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "רישום אוטומטי" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 78367ecd0dd..eff28e11158 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Settings" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Your login session has expired." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a premium member!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,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 organization and choose from available collections." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 6d26e9c3c6b..46ea20d8bfe 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." @@ -404,7 +404,7 @@ "message": "Duljina" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "Minimalna duljina lozinke" }, "uppercase": { "message": "Velika slova (A - Z)" @@ -500,10 +500,10 @@ "message": "Stvori račun" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Postavi jaku lozinku" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" + "message": "Dovrši stvaranje svog računa postavljanjem lozinke" }, "logIn": { "message": "Prijavi se" @@ -527,7 +527,7 @@ "message": "Podsjetnik glavne lozinke (neobavezno)" }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Podsjetnik ti možemo poslati ako zaboraviš svoju lozinku. Najviše $CURRENT$/$MAXIMUM$ znakova.", "placeholders": { "current": { "content": "$1", @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Glavna lozinka" + }, + "masterPassImportant": { + "message": "Glavnu lozinku nije moguće oporaviti ako ju zaboraviš!" + }, + "confirmMasterPassword": { + "message": "Potvrdi glavnu lozinku" + }, + "masterPassHintLabel": { + "message": "Podsjetnik glavne lozinke" + }, + "joinOrganization": { + "message": "Pridruži se organizaciji" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Dovrši pridruživanje organizaciji postavljanjem glavne lozinke." + }, "settings": { "message": "Postavke" }, @@ -574,16 +592,22 @@ } }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Prijava uspješna" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Možeš zatvoriti ovaj prozor" }, "masterPassDoesntMatch": { "message": "Potvrda glavne lozinke se ne podudara." }, "newAccountCreated": { - "message": "Tvoj novi račun je kreiran! Sada se možeš prijaviti." + "message": "Tvoj novi račun je stvoren! Sada se možeš prijaviti." + }, + "newAccountCreated2": { + "message": "Tvoj novi račun je stvoren!" + }, + "youHaveBeenLoggedIn": { + "message": "Prijava uspješna!" }, "masterPassSent": { "message": "Poslali smo e-poštu s podsjetnikom glavne lozinke." @@ -610,11 +634,14 @@ "message": "Kôd za provjeru" }, "confirmIdentity": { - "message": "Potvrdite lozinku za nastavak." + "message": "Za nastavak, potvrdi svoj identitet." }, "verificationCodeRequired": { "message": "Potvrdni kôd je obavezan." }, + "webauthnCancelOrTimeout": { + "message": "Autentifikacija je otkazana ili je trajala predugo. Molimo pokušaj ponovno." + }, "invalidVerificationCode": { "message": "Nevažeći kôd za provjeru" }, @@ -667,17 +694,17 @@ "message": "Autentifikatorska aplikacija" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Unesi kôd generiran autentifikatorskom aplikacijom kao npr. Bitwarden Authenticatorom.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP security key" + "message": "Yubico OTP sigurnosni ključ" }, "yubiKeyDesc": { "message": "Koristi YubiKey za pristup svojem računu. Radi s YubiKey 4, 4 Nano, 4C i NEO uređajima." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Unesi kôd generiran Duo Securityjem.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -694,7 +721,7 @@ "message": "E-pošta" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Unesi kôd poslan e-poštom." }, "loginUnavailable": { "message": "Prijava nije dostupna" @@ -715,13 +742,13 @@ "message": "Navedi osnovni URL svoje lokalno smještene Bitwarden instalacije." }, "selfHostedBaseUrlHint": { - "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" + "message": "Navedi osnovni URL svoje lokalne Bitwarden instalacije, npr.: https://bitwarden.tvrtka.hr" }, "selfHostedCustomEnvHeader": { - "message": "For advanced configuration, you can specify the base URL of each service independently." + "message": "Kao naprednu postavku, možeš odrediti osnovni URL svake usluge zasebno." }, "selfHostedEnvFormInvalid": { - "message": "You must add either the base Server URL or at least one custom environment." + "message": "Moraš dodati ili osnovni URL poslužitelja ili barem jedno prilagođeno okruženje." }, "customEnvironment": { "message": "Prilagođeno okruženje" @@ -772,11 +799,23 @@ "message": "Odjavljen" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "Odjavljen/a si sa svog računa." }, "loginExpired": { "message": "Sesija je istekla." }, + "restartRegistration": { + "message": "Ponovno pokreni registraciju" + }, + "expiredLink": { + "message": "Istekla poveznica" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Ponovno pokreni registraciju ili se pokušaj prijaviti." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Možda već imaš račun" + }, "logOutConfirmation": { "message": "Sigurno se želiš odjaviti?" }, @@ -832,10 +871,10 @@ "message": "Promjeni glavnu lozinku" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Nastavi na web aplikaciju?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Svoju lozinku možeš promijeniti u Bitwarden web aplikaciji." }, "fingerprintPhrase": { "message": "Jedinstvena fraza", @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Možeš kupiti premium članstvo na web trezoru. Želiš li sada posjetiti bitwarden.com?" }, + "premiumPurchaseAlertV2": { + "message": "Premium možeš kupiti u postavkama računa na Bitwarden web aplikaciji." + }, "premiumCurrentMember": { "message": "Ti si premium član!" }, @@ -1243,11 +1285,14 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { - "message": "Access Token Refresh Error" + "message": "Pogreška osvježavanja tokena pristupa" }, "errorRefreshingAccessTokenDesc": { - "message": "No refresh token or API keys found. Please try logging out and logging back in." + "message": "Nije pronađen token za osvježavanje ili API ključevi. Pokušaj se odjaviti i ponovno prijaviti." }, "help": { "message": "Pomoć" @@ -1337,7 +1382,7 @@ "description": "ex. Date this password was updated" }, "exportFrom": { - "message": "Export from" + "message": "Izvezi iz" }, "exportVault": { "message": "Izvezi trezor" @@ -1346,31 +1391,31 @@ "message": "Format datoteke" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "Ova izvozna datoteka biti će zaštićena lozinkom bez koje ju neće biti moguće dešifrirati." }, "filePassword": { - "message": "File password" + "message": "Lozinka datoteke" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "Ova će se lozinka koristiti za izvoz i uvoz ove datoteke" }, "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": "Upotrijebi svoj ključ za šifriranje računa, izveden iz korisničkog imena i glavne lozinke za šifriranje izvoza i ograničavanje uvoza samo na trenutni Bitwarden račun." }, "passwordProtected": { - "message": "Password protected" + "message": "Zaštićeno lozinkom" }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "Bitwarden omogućuje dijeljenje trezora s drugima pomoću organizacijskog računa. Za više informacija posjeti bitwarden. com." }, "exportTypeHeading": { - "message": "Export type" + "message": "Tip izvoza" }, "accountRestricted": { - "message": "Account restricted" + "message": "Račun ograničen" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "Lozinka se ne podudara." }, "hCaptchaUrl": { "message": "hCaptcha Url", @@ -1469,7 +1514,7 @@ "message": "Nerispravan PIN." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "Previše netočnih pokušaja unosa PIN-a. Odjava..." }, "unlockWithWindowsHello": { "message": "Otključaj koristeći Windows Hello" @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "Zahtijevaj Windows Hello pri pokretanju" }, + "autoPromptPolkit": { + "message": "Traži autentifikaciju sustava pri pokretanju" + }, "autoPromptTouchId": { "message": "Zahtijevaj Touch ID pri pokretanju" }, @@ -1592,7 +1646,7 @@ "message": "Trajno izbriši stavku" }, "permanentlyDeleteItemConfirmation": { - "message": "Želiš li zaista trajno izbrisati ovu stavku?" + "message": "Sigurno želiš trajno izbrisati ovu stavku?" }, "permanentlyDeletedItem": { "message": "Stavka trajno izbrisana" @@ -1624,7 +1678,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { - "message": "Verification required", + "message": "Potrebna je potvrda", "description": "Default title for the user verification dialog." }, "currentMasterPass": { @@ -1678,20 +1732,20 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Tvoja nova glavna lozinka ne ispunjava zahtjeve." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Primaj e-poštom od Bitwardena savjete, najave i mogućnosti istraživanja." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "Poništi pretplatu" }, "atAnyTime": { - "message": "at any time." + "message": "bilo kada." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Ako nastaviš, slažeš se s" }, "and": { - "message": "and" + "message": "i" }, "acceptPolicies": { "message": "Označavanjem ove kućice slažete se sa sljedećim:" @@ -1715,10 +1769,10 @@ "message": "Integracija preglednika nije podržana" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Uključi integraciju s web preglednikom" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Pogreška prillikom integracije s preglednikom." }, "browserIntegrationMasOnlyDesc": { "message": "Nažalost, za sada je integracija s preglednikom podržana samo u Mac App Store verziji aplikacije." @@ -1736,10 +1790,10 @@ "message": "Uključi dodatni sloj sigurnosti tražeći verifikaciju jedinstvenom frazom prilikom povezivanja radne površine i preglednika. Kada je ovo uključeno, potrebno je verificirati prilikom svakog novog povezivanja." }, "enableHardwareAcceleration": { - "message": "Use hardware acceleration" + "message": "Koristi hardversko ubrzanje" }, "enableHardwareAccelerationDesc": { - "message": "By default this setting is ON. Turn OFF only if you experience graphical issues. Restart is required." + "message": "Zadano je ova postakva uključena. Isključi samo ako iskusiš probleme s grafikom. Potrebno je ponovno pokretanje." }, "approve": { "message": "Odobri" @@ -1771,6 +1825,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." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Potrebna je potvrda e-pošte" }, + "emailVerifiedV2": { + "message": "e-pošta potvrđena" + }, "emailVerificationRequiredDesc": { "message": "Za korištenje ove značajke, potrebna je ovjera e-pošte." }, @@ -1979,41 +2042,44 @@ "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": "Try again" + "message": "Pokušaj ponovno" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "Za ovu radnju potrebna je potvrda. Postavi PIN za nastavak." }, "setPin": { - "message": "Set PIN" + "message": "Postavi PIN" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Potvrdi biometrijom" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Čekanje potvrde" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "Nije moguće dovršiti biometriju." }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "Koristi drugi način?" }, "useMasterPassword": { - "message": "Use master password" + "message": "Koristi glavnu lozinku" }, "usePin": { - "message": "Use PIN" + "message": "Koristi PIN" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Koristi biometriju" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "Unesi kôd za potvrdu primljen e-poštom." }, "resendCode": { - "message": "Resend code" + "message": "Ponovno pošalji kod" }, "hours": { "message": "sat(i)" @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Vrijeme isteka premašuje ograničenje koju je postavila tvoja organizacija." }, + "inviteAccepted": { + "message": "Pozivnica prihvaćena" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatsko učlanjenje" }, @@ -2133,7 +2202,7 @@ "message": "Promijeni račun" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Već imaš račun?" }, "options": { "message": "Mogućnosti" @@ -2154,10 +2223,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "Izvoz organizacijskog trezora" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Izvest će se samo organizacijski trezor povezan s $ORGANIZATION$. Stavke iz osobnih trezora i stavke iz drugih organizacija neće biti uključene.", "placeholders": { "organization": { "content": "$1", @@ -2230,7 +2299,7 @@ "message": "Generiraj pseudonim e-pošte s vanjskom uslugom prosljeđivanja." }, "forwarderError": { - "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "message": "$SERVICENAME$ greška: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", "placeholders": { "servicename": { @@ -2244,11 +2313,11 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "Generirao Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "Web: $WEBSITE$. Generirao Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -2258,7 +2327,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "Nevažeći $SERVICENAME$ API token", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -2268,7 +2337,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "Nevažeći $SERVICENAME$ API token: $ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -2282,7 +2351,7 @@ } }, "forwarderNoAccountId": { - "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "message": "Nije moguće dobiti $SERVICENAME$ maskirani ID računa e-pošte.", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -2292,7 +2361,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "Nevažeća $SERVICENAME$ domena.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -2302,7 +2371,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "Nevažeći $SERVICENAME$ URL.", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -2312,7 +2381,7 @@ } }, "forwarderUnknownError": { - "message": "Unknown $SERVICENAME$ error occurred.", + "message": "Nepoznata $SERVICENAME$ greška.", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -2322,7 +2391,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Unknown forwarder: '$SERVICENAME$'.", + "message": "Nepoznati prosljeditelj: '$SERVICENAME$.", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -2482,25 +2551,25 @@ "message": "Zatražena je prijava" }, "creatingAccountOn": { - "message": "Creating account on" + "message": "Stvaranje računa na" }, "checkYourEmail": { - "message": "Check your email" + "message": "Provjeri svoju e-poštu" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "Slijedi vezu u e-pošti poslanoj na" }, "andContinueCreatingYourAccount": { - "message": "and continue creating your account." + "message": "za nastavak stvaranja tvojeg računa." }, "noEmail": { - "message": "No email?" + "message": "Nema e-pošte?" }, "goBack": { - "message": "Go back" + "message": "Nazad" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "na uređivanje svoje adrese e-pošte." }, "exposedMasterPassword": { "message": "Ukradena glavna lozinka" @@ -2521,10 +2590,10 @@ "message": "Važno:" }, "accessTokenUnableToBeDecrypted": { - "message": "You have been logged out because your access token could not be decrypted. Please log in again to resolve this issue." + "message": "Napravljena je odjava ste jer se tvoj pristupni token nije mogao dešifrirati. Ponovno se prijavi." }, "refreshTokenSecureStorageRetrievalFailure": { - "message": "You have been logged out because your refresh token could not be retrieved. Please log in again to resolve this issue." + "message": "Napravljena je odjava ste jer se tvoj token za osvježavanje nije mogao dešifrirati. Ponovno se prijavi." }, "masterPasswordHint": { "message": "Glavnu lozinku nije moguće oporaviti ako ju zaboraviš!" @@ -2706,10 +2775,10 @@ "message": "Podizbornik" }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "U/Isključi bočnu navigaciju" }, "skipToContent": { - "message": "Skip to content" + "message": "Preskoči na sadržaj" }, "typePasskey": { "message": "Pristupni ključ" @@ -2763,14 +2832,17 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Greška pri povezivanju s uslugom Duo. Koristi drugu metodu prijave s dvostrukom autentifikacijom ili kontaktiraj Duo za pomoć." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "Pokreni Duo i slijedi korake za dovršetak prijave." }, "duoRequiredByOrgForAccount": { - "message": "Duo two-step login is required for your account." + "message": "Za tvoj račun je potrebna Duo dvostruka autentifikacija." }, "launchDuo": { - "message": "Launch Duo in Browser" + "message": "Pokreni Duo u pregledniku" }, "importFormatError": { "message": "Podaci nisu ispravno formatirani. Provjeri uvoznu datoteku i pokušaj ponovno." @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Nesipravna lozinka datoteke. Unesi lozinku izvozne datoteke." }, - "importDestination": { - "message": "Odredište uvoza" + "destination": { + "message": "Odredište" }, "learnAboutImportOptions": { "message": "Više o mogućnostima uvoza" @@ -2844,7 +2916,7 @@ "message": "Potvrdi lozinku datoteke" }, "exportSuccess": { - "message": "Vault data exported" + "message": "Podaci iz trezora su izvezeni" }, "multifactorAuthenticationCancelled": { "message": "Multifaktorska autentifikacija otkazana" @@ -2856,13 +2928,13 @@ "message": "Neispravno korisničko ime ili lozinka" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "Neispravna lozinka" }, "incorrectCode": { - "message": "Incorrect code" + "message": "Neispravan kôd" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "Neispravan PIN" }, "multifactorAuthenticationFailed": { "message": "Multifaktorska autentifikacija nije uspjela" @@ -2886,7 +2958,7 @@ "message": "Odobri svoj zahtjev za prijavu u svojoj aplikaciji za autentifikaciju ili unesi jednokratni kôd." }, "passcode": { - "message": "Passcode" + "message": "Jednokratni kôd" }, "lastPassMasterPassword": { "message": "LastPass glavna lozinka" @@ -2920,35 +2992,35 @@ "message": "Umetni YubiKey pridružen svojem LastPass računu u USB priključak račuanala, a zatim dodirni njegovu tipku." }, "commonImportFormats": { - "message": "Common formats", + "message": "Uobičajeni oblici", "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Uspješno" }, "troubleshooting": { - "message": "Troubleshooting" + "message": "Rješavanje problema" }, "disableHardwareAccelerationRestart": { - "message": "Disable hardware acceleration and restart" + "message": "Isključi hardversko ubrzanje i ponovno pokreni" }, "enableHardwareAccelerationRestart": { - "message": "Enable hardware acceleration and restart" + "message": "Uključi hardversko ubrzanje i ponovno pokreni" }, "removePasskey": { - "message": "Remove passkey" + "message": "Ukloni pristupni ključ" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Pristupni ključ uklonjen" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Greška pri dodjeljivanju ciljne zbirke." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Greška pri dodjeljivanju ciljne mape." }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "Pogledaj stavke u $NAME$", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -2958,7 +3030,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "Natrag na $NAME$", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -2968,11 +3040,11 @@ } }, "back": { - "message": "Back", + "message": "Natrag", "description": "Button text to navigate back" }, "removeItem": { - "message": "Remove $NAME$", + "message": "Ukloni $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Podaci" + }, + "fileSends": { + "message": "Send datoteke" + }, + "textSends": { + "message": "Send tekstovi" + }, + "ssoError": { + "message": "Nisu nađeni slobodni portovi za SSO prijavu." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 14c97b81fe5..75fad919cd3 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Mesterjelszó" + }, + "masterPassImportant": { + "message": "A mesterjelszó nem állítható helyre, ha elfelejtik!" + }, + "confirmMasterPassword": { + "message": "Mesterjelszó megerősítése" + }, + "masterPassHintLabel": { + "message": "Mesterjelszó emlékeztető" + }, + "joinOrganization": { + "message": "Csatlakozás szervezethez" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Fejezzük be a szervezethez csatlakozást egy mesterjelszó beállításával." + }, "settings": { "message": "Beállítások" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "A fiók létrehozásra került. Most már be lehet jelentkezni." }, + "newAccountCreated2": { + "message": "Az új fiók létrrejött." + }, + "youHaveBeenLoggedIn": { + "message": "Megtörtént a bejelentkezés!" + }, "masterPassSent": { "message": "Elküldtünk neked egy emailt a mesterjelszó emlékeztetővel." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Az ellenőrző kód kötelező." }, + "webauthnCancelOrTimeout": { + "message": "A hitelesítés megszakításra került vagy túl sokáig tartott. Próbáljuk újra." + }, "invalidVerificationCode": { "message": "Érvénytelen ellenőrző kód" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "A bejelentkezési munkamenet lejárt." }, + "restartRegistration": { + "message": "Regisztráció újra indítása" + }, + "expiredLink": { + "message": "A hivatkozás lejárt." + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "A Bitwarden képes tárolni és kitölteni a kétlépcsős ellenőrző kódokat. Válasszuk a kamera ikont, hogy képernyőképet készítsünk a webhely hitelesítő QR kódjáról vagy másoljuk ki és illesszük be a kulcsot ebbe a mezőbe." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Már rendelkezünkk fiókkal." + }, "logOutConfirmation": { "message": "Biztosan szeretnénk kijelentkezni?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Prémium tagságot a bitwarden.com webes széfben lehet vásárolni. Szeretnénk most felkeresni a webhelyet?" }, + "premiumPurchaseAlertV2": { + "message": "Prémium szolgáltatást vásárolhatunk a Bitwarden webalkalmazás fiókbeállításai között." + }, "premiumCurrentMember": { "message": "Jelenleg a prémium tagság érvényben van." }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Hozzáférési vezérjel frissítési hiba" }, @@ -1477,9 +1522,15 @@ "additionalWindowsHelloSettings": { "message": "Kiegészítő Windows Hello beállítások" }, + "unlockWithPolkit": { + "message": "Feloldás rendszer hitelesítéssel" + }, "windowsHelloConsentMessage": { "message": "Bitwarden ellenőrzés." }, + "polkitConsentMessage": { + "message": "Hitelesítés a Bitwarden feloldásához." + }, "unlockWithTouchId": { "message": "Feloldás Touch ID segítségével" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "Windows Hello kérése indításkor" }, + "autoPromptPolkit": { + "message": "Rendszer hiteletesítés bekérése indításkor" + }, "autoPromptTouchId": { "message": "Érintés AZ kérése indításkor" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Az új mesterjelszó nem felel meg a szabály követelményeknek." }, - "receiveMarketingEmails": { - "message": "Emaileket kaphatunk a Bitwardentől bejelentésekről, tanácsokról és kutatási lehetőségekről." + "receiveMarketingEmailsV2": { + "message": "Tanácsokat, bejelentéseket és kutatási lehetőségeket kaphatunk a Bitwardentől a postaládába." }, "unsubscribe": { "message": "Leiratkozás" @@ -1771,6 +1825,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": "Az automatikus beüzemelés nem érhető el." + }, + "biometricsManualSetupDesc": { + "message": "A telepítési mód miatt a biometrikus adatok támogatása nem volt automatikusan engedélyezhető. Szeretnénk megnyitni a dokumentációt arról, hogyan kell ezt manuálisan megtenni?" + }, "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." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email hitelesítés szükséges" }, + "emailVerifiedV2": { + "message": "Az email cím ellenőrzésre került." + }, "emailVerificationRequiredDesc": { "message": "A funkció használatához ellenőrizni kell az email címet." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "A széf időkorlátja túllépi a szervezet által beállított korlátozást." }, + "inviteAccepted": { + "message": "A meghívás elfogadásra került." + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatikus regisztráció" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Hiba történt a Duo szolgáltatáshoz csatlakozáskor. Használjunk másik kétlépcsős bejelentkezési módot vagy kérjünk segítséget a Duotól." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Indítsuk el a DUO-t és kövessük a lépéseket a bejelentkezés befejezéséhez." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "A fájl jelszó érvénytelen. Használjuk az exportfájl létrehozásakor megadott jelszót." }, - "importDestination": { - "message": "Importálás leírás" + "destination": { + "message": "Cél" }, "learnAboutImportOptions": { "message": "Információ az importálási opciókról" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Adat" + }, + "fileSends": { + "message": "Fájl küldés" + }, + "textSends": { + "message": "Szöveg küldés" + }, + "ssoError": { + "message": "Nem található szabad port az sso bejelentkezéshez." + }, + "fileSavedToDevice": { + "message": "A fájl mentésre került az eszközre. Kezeljük az eszközről a letöltéseket." } } diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 574ab5bb6b2..15d11730eb8 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Setelan" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Akun baru Anda telah dibuat! Sekarang Anda bisa masuk." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "Kami telah mengirimi Anda email dengan petunjuk sandi utama Anda." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Kode verifikasi diperlukan." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Kode verifikasi tidak valid" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Sesi masuk Anda telah berakhir." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Anda yakin ingin keluar?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Kamu bisa membeli keanggotaan Premium di Brankas Web bitwarden.com. Apakah kamu ingin mengunjungi situs web itu sekarang?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Anda adalah anggota premium!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "Minta Windows Hello saat diluncurkan" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "Minta Touch ID saat diluncurkan" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Kata sandi utama Anda yang baru tidak memenuhi persyaratan kebijakan." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "Biometrik browser mengharuskan biometrik desktop diaktifkan di pengaturan terlebih dahulu." }, + "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": "Karena Kebijakan Perusahaan, Anda dilarang menyimpan item ke lemari besi pribadi Anda. Ubah opsi Kepemilikan ke organisasi dan pilih dari Koleksi yang tersedia." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Verifikasi Email Diperlukan" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Anda harus mengkonfirmasi email anda untuk menggunakan fitur ini." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Waktu tunggu brankas Anda melebihi batasan yang diatur organisasi Anda." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Pendaftaran otomatis" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index c0c1e322c65..7e5577b30d8 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -500,10 +500,10 @@ "message": "Crea account" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Imposta una password robusta" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" + "message": "Termina la creazione del tuo account impostando una password" }, "logIn": { "message": "Accedi" @@ -527,7 +527,7 @@ "message": "Suggerimento per la password principale (facoltativo)" }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Se dimentichi la password, il suggerimento password può essere inviato alla tua email. $CURRENT$/$MAXIMUM$ massimo carattere.", "placeholders": { "current": { "content": "$1", @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Password principale" + }, + "masterPassImportant": { + "message": "La tua password principale non può essere recuperata se la dimentichi!" + }, + "confirmMasterPassword": { + "message": "Conferma password principale" + }, + "masterPassHintLabel": { + "message": "Suggerimento per la password principale" + }, + "joinOrganization": { + "message": "Unisciti all'organizzazione" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Termina l'adesione a questa organizzazione impostando una password principale." + }, "settings": { "message": "Impostazioni" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Il tuo nuovo account è stato creato! Ora puoi eseguire l'accesso." }, + "newAccountCreated2": { + "message": "Il tuo nuovo account è stato creato!" + }, + "youHaveBeenLoggedIn": { + "message": "Hai effettuato l'accesso!" + }, "masterPassSent": { "message": "Ti abbiamo inviato un'email con il tuo suggerimento per la password principale." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Codice di verifica obbligatorio." }, + "webauthnCancelOrTimeout": { + "message": "L'autenticazione è stata annullata o ha richiesto troppo tempo. Per favore riprova." + }, "invalidVerificationCode": { "message": "Codice di verifica non valido" }, @@ -667,17 +694,17 @@ "message": "App di autenticazione" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Inserisci un codice generato da un'app di autenticazione come Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP security key" + "message": "Chiave di sicurezza YubiKey OTP" }, "yubiKeyDesc": { "message": "Usa YubiKey per accedere al tuo account. Compatibile con YubiKey 4, 4 Nano, 4C, e dispositivi NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Inserisci un codice generato da Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -694,7 +721,7 @@ "message": "Email" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Inserisci il codice inviato alla tua email." }, "loginUnavailable": { "message": "Login non disponibile" @@ -777,6 +804,18 @@ "loginExpired": { "message": "La tua sessione è scaduta." }, + "restartRegistration": { + "message": "Riprova la registrazione" + }, + "expiredLink": { + "message": "Link scaduto" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Riavvia la registrazione o prova ad accedere." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Potresti già avere un account" + }, "logOutConfirmation": { "message": "Sei sicuro di voler uscire?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Puoi acquistare il un abbonamento Premium dalla cassaforte web su bitwarden.com. Vuoi visitare il sito?" }, + "premiumPurchaseAlertV2": { + "message": "Puoi acquistare Premium dalle impostazioni del tuo account sull'app web Bitwarden." + }, "premiumCurrentMember": { "message": "Sei un membro Premium!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Errore di aggiornamento del token di accesso" }, @@ -1477,9 +1522,15 @@ "additionalWindowsHelloSettings": { "message": "Impostazioni aggiuntive di Windows Hello" }, + "unlockWithPolkit": { + "message": "Sblocca con l'autenticazione di sistema" + }, "windowsHelloConsentMessage": { "message": "Verifica per Bitwarden." }, + "polkitConsentMessage": { + "message": "Autenticazione per sbloccare Bitwarden." + }, "unlockWithTouchId": { "message": "Sblocca con Touch ID" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "Richiedi Windows Hello all'avvio" }, + "autoPromptPolkit": { + "message": "Chiedi autenticazione di sistema all'avvio" + }, "autoPromptTouchId": { "message": "Richiedi Touch ID all'avvio" }, @@ -1678,20 +1732,20 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "La tua nuova password principale non soddisfa i requisiti di sicurezza." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Ottieni consigli, annunci e opportunità di ricerca da Bitwarden nella tua casella di posta." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "Annulla iscrizione" }, "atAnyTime": { - "message": "at any time." + "message": "in qualsiasi momento." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Continuando accetti le" }, "and": { - "message": "and" + "message": "e" }, "acceptPolicies": { "message": "Selezionando questa casella accetti quanto segue:" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "L'autenticazione biometrica del browser richiede che l'autenticazione biometrica del desktop sia stata già impostata nelle impostazioni." }, + "biometricsManualSetupTitle": { + "message": "Configurazione automatica non disponibile" + }, + "biometricsManualSetupDesc": { + "message": "A causa del metodo di installazione, il supporto biometrico non può essere attivato automaticamente. Aprire la documentazione su come farlo manualmente?" + }, "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." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Verifica email obbligatoria" }, + "emailVerifiedV2": { + "message": "Email verificata" + }, "emailVerificationRequiredDesc": { "message": "Devi verificare la tua email per usare questa funzionalità." }, @@ -1979,6 +2042,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": "La tua organizzazione ha disabilitato la crittografia affidabile del dispositivo. Per favore imposta una password principale per accedere alla tua cassaforte." + }, "tryAgain": { "message": "Riprova" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Il timeout della tua cassaforte supera i limiti impostati dalla tua organizzazione." }, + "inviteAccepted": { + "message": "Invito accettato" + }, "resetPasswordPolicyAutoEnroll": { "message": "Iscrizione automatica" }, @@ -2706,7 +2775,7 @@ "message": "Sottomenu" }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "Attiva/Disattiva navigazione laterale" }, "skipToContent": { "message": "Vai al contenuto" @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Errore di connessione con il servizio Duo. Utilizza un metodo di login in due passaggi diverso o contatta Duo per assistenza." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Avvia Duo e segui i passaggi per finire di accedere." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Password errata, usa la password che hai inserito alla creazione del file di esportazione." }, - "importDestination": { - "message": "Destinazione dell'importazione" + "destination": { + "message": "Destinazione" }, "learnAboutImportOptions": { "message": "Ulteriori informazioni sulle tue opzioni di importazione" @@ -2844,7 +2916,7 @@ "message": "Conferma password del file" }, "exportSuccess": { - "message": "Vault data exported" + "message": "Dati della cassaforte esportati" }, "multifactorAuthenticationCancelled": { "message": "Verifica in due passaggi annullata" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Dati" + }, + "fileSends": { + "message": "Send File" + }, + "textSends": { + "message": "Send Testo" + }, + "ssoError": { + "message": "Non è stato possibile trovare nessuna porta libera per il login Sso." + }, + "fileSavedToDevice": { + "message": "File salvato sul dispositivo. Gestisci dai download del dispositivo." } } diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index 80060295411..2ca601a6a8b 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "マスターパスワード" + }, + "masterPassImportant": { + "message": "マスターパスワードを忘れた場合は復元できません!" + }, + "confirmMasterPassword": { + "message": "マスターパスワードの確認" + }, + "masterPassHintLabel": { + "message": "マスターパスワードのヒント" + }, + "joinOrganization": { + "message": "組織に参加" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "マスターパスワードを設定して、この組織への参加を完了します。" + }, "settings": { "message": "設定" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "新しいアカウントを作成しました!今すぐログインできます。" }, + "newAccountCreated2": { + "message": "新しいアカウントを作成しました!" + }, + "youHaveBeenLoggedIn": { + "message": "ログインしました!" + }, "masterPassSent": { "message": "あなたのマスターパスワードのヒントを記載したメールを送信しました。" }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "認証コードは必須項目です。" }, + "webauthnCancelOrTimeout": { + "message": "認証がキャンセルされたか、時間がかかりすぎました。もう一度やり直してください。" + }, "invalidVerificationCode": { "message": "認証コードが間違っています" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "ログインセッションの有効期限が切れています。" }, + "restartRegistration": { + "message": "登録を再度始める" + }, + "expiredLink": { + "message": "期限切れのリンク" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "登録を再度始めるか、ログインしてください。" + }, + "youMayAlreadyHaveAnAccount": { + "message": "すでにアカウントを持っている可能性があります" + }, "logOutConfirmation": { "message": "ログアウトしてもよろしいですか?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "プレミアム会員権は bitwarden.com ウェブ保管庫で購入できます。ウェブサイトを開きますか?" }, + "premiumPurchaseAlertV2": { + "message": "Bitwarden ウェブアプリでアカウント設定からプレミアムを購入できます。" + }, "premiumCurrentMember": { "message": "あなたはプレミアム会員です!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "コピーしました" + }, "errorRefreshingAccessToken": { "message": "アクセストークンの更新エラー" }, @@ -1477,9 +1522,15 @@ "additionalWindowsHelloSettings": { "message": "追加の Windows Hello 設定" }, + "unlockWithPolkit": { + "message": "システム認証でロック解除" + }, "windowsHelloConsentMessage": { "message": "Bitwarden の認証を行います。" }, + "polkitConsentMessage": { + "message": "認証して Bitwarden のロックを解除します。" + }, "unlockWithTouchId": { "message": "Touch ID でロック解除" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "起動時に Windows Hello を要求する" }, + "autoPromptPolkit": { + "message": "起動時にシステム認証を要求する" + }, "autoPromptTouchId": { "message": "起動時に Touch ID を要求する" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "新しいマスターパスワードは最低要件を満たしていません。" }, - "receiveMarketingEmails": { - "message": "Bitwarden からのお知らせ、アドバイス、アンケート調査等のメールを受信します。" + "receiveMarketingEmailsV2": { + "message": "Bitwarden からメールでアドバイスやお知らせ、リサーチの機会を受け取りましょう。" }, "unsubscribe": { "message": "配信停止" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "ブラウザで生体認証を利用するには、最初に設定でデスクトップ生体認証を有効にする必要があります。" }, + "biometricsManualSetupTitle": { + "message": "自動設定は利用できません" + }, + "biometricsManualSetupDesc": { + "message": "インストール方法により、生体認証を自動的に有効化できませんでした。手動で設定する方法の説明を開きますか?" + }, "personalOwnershipSubmitError": { "message": "組織のポリシーにより、個人保管庫へのアイテムの保存が制限されています。 所有権を組織に変更し、利用可能なコレクションから選択してください。" }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "メールアドレスの確認が必要です" }, + "emailVerifiedV2": { + "message": "メールアドレスを認証しました" + }, "emailVerificationRequiredDesc": { "message": "この機能を使用するにはメールアドレスを確認する必要があります。" }, @@ -1979,6 +2042,9 @@ "updateWeakMasterPasswordWarning": { "message": "マスターパスワードが組織のポリシーに適合していません。保管庫にアクセスするには、今すぐマスターパスワードを更新しなければなりません。続行すると現在のセッションからログアウトし、再度ログインする必要があります。 他のデバイス上のアクティブなセッションは、最大1時間アクティブであり続けることがあります。" }, + "tdeDisabledMasterPasswordRequired": { + "message": "あなたの組織は信頼できるデバイスの暗号化を無効化しました。保管庫にアクセスするにはマスターパスワードを設定してください。" + }, "tryAgain": { "message": "再試行" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "保管庫のタイムアウトが組織によって設定された制限を超えています。" }, + "inviteAccepted": { + "message": "招待が承認されました" + }, "resetPasswordPolicyAutoEnroll": { "message": "自動登録" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Duo サービスへの接続中にエラーが発生しました。異なる二段階ログイン方法を使用するか、Duo に連絡してください。" + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "ログインを完了するには Duo を起動し手順に従ってください。" }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "無効なファイルパスワードです。エクスポートファイルを作成したときに入力したパスワードを使用してください。" }, - "importDestination": { - "message": "インポート先" + "destination": { + "message": "保存先" }, "learnAboutImportOptions": { "message": "インポートオプションの詳細" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "データ" + }, + "fileSends": { + "message": "ファイル Send" + }, + "textSends": { + "message": "テキスト Send" + }, + "ssoError": { + "message": "SSO ログインのための空きポートが見つかりませんでした。" + }, + "fileSavedToDevice": { + "message": "ファイルをデバイスに保存しました。デバイスのダウンロードで管理できます。" } } diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index 0887d769823..9194fd7c22a 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Settings" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Your login session has expired." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a premium member!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,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 organization and choose from available collections." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 0887d769823..9194fd7c22a 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Settings" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Your login session has expired." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a premium member!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,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 organization and choose from available collections." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index ae4d9150af5..b19fe2730fb 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "ಸೆಟ್ಟಿಂಗ್‍ಗಳು" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "ನಿಮ್ಮ ಹೊಸ ಖಾತೆಯನ್ನು ರಚಿಸಲಾಗಿದೆ! ನೀವು ಈಗ ಲಾಗ್ ಇನ್ ಮಾಡಬಹುದು." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "ನಿಮ್ಮ ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್ ಸುಳಿವಿನೊಂದಿಗೆ ನಾವು ನಿಮಗೆ ಇಮೇಲ್ ಕಳುಹಿಸಿದ್ದೇವೆ." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "ಪರಿಶೀಲನೆ ಕೋಡ್ ಅಗತ್ಯವಿದೆ." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "ನಿಮ್ಮ ಲಾಗಿನ್ ಸೆಷನ್ ಅವಧಿ ಮೀರಿದೆ." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "ಲಾಗ್ ಔಟ್ ಮಾಡಲು ನೀವು ಖಚಿತವಾಗಿ ಬಯಸುವಿರಾ?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "ನೀವು ಬಿಟ್ವಾರ್ಡೆನ್.ಕಾಮ್ ವೆಬ್ ವಾಲ್ಟ್ನಲ್ಲಿ ಪ್ರೀಮಿಯಂ ಸದಸ್ಯತ್ವವನ್ನು ಖರೀದಿಸಬಹುದು. ನೀವು ಈಗ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಭೇಟಿ ನೀಡಲು ಬಯಸುವಿರಾ?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "ನೀವು ಪ್ರೀಮಿಯಂ ಸದಸ್ಯರಾಗಿದ್ದೀರಿ!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,15 @@ "additionalWindowsHelloSettings": { "message": "Additional Windows Hello settings" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "ಬಿಟ್‌ವಾರ್ಡೆನ್‌ಗಾಗಿ ಪರಿಶೀಲಿಸಿ." }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "ಟಚ್ ಐಡಿ ಯೊಂದಿಗೆ ಅನ್ಲಾಕ್ ಮಾಡಿ" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "ನಿಮ್ಮ ಹೊಸ ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್ ನೀತಿಯ ಅವಶ್ಯಕತೆಗಳನ್ನು ಪೂರೈಸುವುದಿಲ್ಲ." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "ಬ್ರೌಸರ್ ಬಯೋಮೆಟ್ರಿಕ್ಸ್ ಮೊದಲು ಸೆಟ್ಟಿಂಗ್ಗಳಲ್ಲಿ ಡೆಸ್ಕ್ಟಾಪ್ ಬಯೋಮೆಟ್ರಿಕ್ ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಬೇಕಾಗುತ್ತದೆ." }, + "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." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "ಇಮೇಲ್ ಪರಿಶೀಲನೆ ಅಗತ್ಯವಿದೆ" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "ಈ ವೈಶಿಷ್ಟ್ಯವನ್ನು ಬಳಸಲು ನಿಮ್ಮ ಇಮೇಲ್ ಅನ್ನು ನೀವು ಪರಿಶೀಲಿಸಬೇಕು." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 63d03f5ebf9..5efb24b9054 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "설정" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "계정 생성이 완료되었습니다! 이제 로그인하실 수 있습니다." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "마스터 비밀번호 힌트가 담긴 이메일을 보냈습니다." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "인증 코드는 반드시 입력해야 합니다." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "유효하지 않은 확인 코드" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "로그인 세션이 만료되었습니다." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "정말 로그아웃하시겠습니까?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "bitwarden.com 웹 보관함에서 프리미엄 멤버십을 구입할 수 있습니다. 지금 웹 사이트를 방문하시겠습니까?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "프리미엄 사용자입니다!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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를 사용하여 잠금 해제" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "실행 시 Windows Hello 요구하기" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "실행 시 Touch ID 요구하기" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "새 마스터 비밀번호가 정책 요구 사항을 따르지 않습니다." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "브라우저에서 생체 인식을 사용하기 위해서는 설정에서 데스크톱 생체 인식을 먼저 활성화해야 합니다." }, + "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": "엔터프라이즈 정책으로 인해 개인 보관함에 항목을 저장할 수 없습니다. 조직에서 소유권 설정을 변경한 다음, 사용 가능한 컬렉션 중에서 선택해주세요." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "이메일 인증 필요함" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "이 기능을 이용하기 위해서는 이메일을 인증해야 합니다." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "볼트 제한 시간이 조직에서 설정한 제한을 초과합니다." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "자동 등록" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index e1020f7d237..6e94ed65b8e 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Nustatymai" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Jūsų paskyra sukurta! Galite prisijungti." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "Išsiuntėme jums el. laišką su pagrindinio slaptažodžio užuomina." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Būtinas patvirtinimo kodas." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Neteisingas patvirtinimo kodas" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Sesijos laikas baigėsi." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Ar tikrai norite atsijungti?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Galite įsigyti Premium narystę bitwarden.com interneto saugykloje. Ar norite aplankyti svetainę dabar?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Esate Premium narys!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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ę" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Jūsų naujasis pagrindinis slaptažodis neatitinka politikos reikalavimų." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "Pirma reikia nustatymuose nustatyti darbalaukio biometrinius duomenys, prieš juos naudojant naršyklėje." }, + "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": "Dėl įmonės politikos jums neleidžiama saugoti daiktų asmeninėje saugykloje. Pakeiskite nuosavybės parinktį į organizaciją ir pasirinkite iš galimų rinkinių." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Reikalingas elektroninio pašto patvirtinimas" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Privalote patvirtinti savo el. paštą norint naudotis šia funkcija." }, @@ -1979,6 +2042,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ą" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Jūsų saugyklos skirtasis laikas viršija jūsų organizacijos nustatytus apribojimus." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatinis įtraukimas" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Netinkamas failo slaptažodis, prašome naudoti tą slaptažodį, kurį įvedėte kurdami eksportuojamą failą." }, - "importDestination": { - "message": "Importavimo vieta" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Sužinoti apie importavimo pasirinkimus" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 64c27f9f627..081ff83b9f4 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -503,7 +503,7 @@ "message": "Jāiestata droša parole" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Jāpabeidz sava konta izveida ar paroles iestatīšanu" + "message": "Sava konta izveidošana jāpabeidz ar paroles iestatīšanu" }, "logIn": { "message": "Pieteikties" @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Galvenā parole" + }, + "masterPassImportant": { + "message": "Galveno paroli nevar atgūt, ja tā tiek aizmirsta." + }, + "confirmMasterPassword": { + "message": "Apstiprināt galveno paroli" + }, + "masterPassHintLabel": { + "message": "Galvenās paroles norāde" + }, + "joinOrganization": { + "message": "Pievienoties apvienībai" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Pabeigt pievienošanos šai apvienībai ar galvenās paroles iestatīšanu." + }, "settings": { "message": "Iestatījumi" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Jaunais konts ir izveidots. Tagad vari pieteikties." }, + "newAccountCreated2": { + "message": "Jaunais konts tika izveidots." + }, + "youHaveBeenLoggedIn": { + "message": "Tu esi pieteicies." + }, "masterPassSent": { "message": "Mēs nosūtījām galvenās paroles norādi e-pastā." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Ir nepieciešams apstiprinājuma kods." }, + "webauthnCancelOrTimeout": { + "message": "Autentifikācija tika atcelta vai tā aizņēma pārāk daudz laika. Lūgums mēģināt vēlreiz." + }, "invalidVerificationCode": { "message": "Nederīgs apstiprinājuma kods" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Pieteikšanās sesija ir beigusies." }, + "restartRegistration": { + "message": "Sākt reģistrēšanos no jauna" + }, + "expiredLink": { + "message": "Saitei beidzies derīgums" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Lūgums sākt reģistrēšanos no jauna vai mēģināt pieteikties." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Tev jau varētu būt konts" + }, "logOutConfirmation": { "message": "Vai tiešām atteikties?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Premium dalību ir iespējams iegādāties bitwarden.com tīmekļa glabātavā. Vai tagad apmeklēt tīmekļvietni?" }, + "premiumPurchaseAlertV2": { + "message": "Premium var iegādāties Bitwarden tīmekļa lietotnē sava konta iestatījumos." + }, "premiumCurrentMember": { "message": "Jūs esat premium dalībnieks!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Ievietošana starpliktuvē veiksmīga" + }, "errorRefreshingAccessToken": { "message": "Piekļuves pilnvaras atsvaizināšanas kļūda" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Jaunā galvenā parole neatbilst nosacījumu prasībām." }, - "receiveMarketingEmails": { - "message": "Saņemt e-pasta ziņojumus no Bitwarden par paziņojumiem, padomiem un izpētes iespējām." + "receiveMarketingEmailsV2": { + "message": "Iegūt savā iesūtnē padomus, paziņojumus un izpētes iespējas no Bitwarden." }, "unsubscribe": { "message": "Atteikt abonēšanu" @@ -1771,6 +1825,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." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Nepieciešama e-pasta adreses apstiprināšana" }, + "emailVerifiedV2": { + "message": "E-pasta adrese ir apliecināta" + }, "emailVerificationRequiredDesc": { "message": "Ir jāapstiprina e-pasta adrese, lai izmantotu šo iespēju." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Glabātavas noildze pārsniedz apvienības uzstādītos ierobežojumus." }, + "inviteAccepted": { + "message": "Uzaicinājums apstiprināts" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automātiska ievietošana sarakstā" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Kļūda savienojuma izveidošanā ar Duo pakalpojumu. Jāizmanto cits divpakāpju pieteikšanāš veids vai jāvēršas pie Duo pēc palīdzības." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Jāpalaiž Duo un jāseko soļiem, lai pabeigtu pieteikšanos." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Nederīga datnes parole, lūgums izmantot to paroli, kas tika ievadīta izgūšanas datnes izveidošanas brīdī." }, - "importDestination": { - "message": "Ievietošanas galamērķis" + "destination": { + "message": "Galamērķis" }, "learnAboutImportOptions": { "message": "Uzzināt par ievietošanas iespējām" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Dati" + }, + "fileSends": { + "message": "Datņu Send" + }, + "textSends": { + "message": "Teksta Send" + }, + "ssoError": { + "message": "Netika atrasti brīvi vienotās (SSO) pieteikšanās porti." + }, + "fileSavedToDevice": { + "message": "Datne saglabāta ierīcē. Tā ir atrodama ierīces lejupielāžu mapē." } } diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index 6e02878b12d..848d796f146 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Podešavanja" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Vaš novi nalog je kreiran! Sada se možete prijaviti." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "Poslali smo vam email sa podsjetnikom na glavnu lozinku." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Potreban je verifikacioni kod." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Vaša sesija je istekla." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Jeste li sigurni da se želite odjaviti?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Premium članstvo možete kupiti u trezoru na internet strani bitwarden.com. Da li želite da posjetite internet lokaciju sada?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Vi ste premijum član!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,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 organization and choose from available collections." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 880cc5b0848..abc7d873e44 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "ക്രമീകരണങ്ങള്‍" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "തങ്ങളുടെ അക്കൗണ്ട് സൃഷ്ടിക്കപ്പെട്ടു. ഇനി താങ്കൾക്ക് ലോഗിൻ ചെയ്യാം." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "നിങ്ങളുടെ പ്രാഥമിക പാസ്‌വേഡ് സൂചനയുള്ള ഒരു ഇമെയിൽ ഞങ്ങൾ നിങ്ങൾക്ക് അയച്ചു." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "പരിശോധിച്ചുറപ്പിക്കൽ കോഡ് ആവശ്യമാണ്." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "നിങ്ങളുടെ പ്രവർത്തന സമയം കഴിഞ്ഞിരിക്കുന്നു." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "നിങ്ങൾക്ക് ലോഗ് ഔട്ട് ചെയ്യണമെന്ന് ഉറപ്പാണോ?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "നിങ്ങൾക്ക് bitwarden.com വെബ് വാൾട്ടിൽ പ്രീമിയം അംഗത്വം വാങ്ങാം. നിങ്ങൾക്ക് ഇപ്പോൾ വെബ്സൈറ്റ് സന്ദർശിക്കാൻ ആഗ്രഹമുണ്ടോ?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "തങ്ങൾ ഒരു പ്രീമിയം അംഗമാണ്!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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 ഉപയോഗിച്ച് അൺലോക്കുചെയ്യുക" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "നിങ്ങളുടെ പുതിയ മാസ്റ്റർ പാസ്‌വേഡ് നയ ആവശ്യകതകൾ നിറവേറ്റുന്നില്ല." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,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 organization and choose from available collections." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 0887d769823..9194fd7c22a 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Settings" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Your login session has expired." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a premium member!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,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 organization and choose from available collections." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index d007a4e0f45..5f2ec86f595 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Settings" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Your login session has expired." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a premium member!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,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 organization and choose from available collections." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index 03d6c95a293..335a7ceec5b 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" @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Bli med i organisasjon" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Innstillinger" }, @@ -577,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." @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Din nye konto har blitt opprettet! Du kan nå logge på." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "Vi har sendt deg en E-post med hintet til superpassordet." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "En verifiseringskode er påkrevd." }, + "webauthnCancelOrTimeout": { + "message": "Autentiseringen ble avbrutt eller tok for lang tid. Prøv igjen." + }, "invalidVerificationCode": { "message": "Ugyldig verifiseringskode" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Din innloggingsøkt har utløpt." }, + "restartRegistration": { + "message": "Start registrering på nytt" + }, + "expiredLink": { + "message": "Utløpt lenke" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Er du sikker på at du vil logge av?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Du kan kjøpe et Premium-medlemskap på bitwarden.net-netthvelvet. Vil du besøke det nettstedet nå?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Du er et Premium-medlem!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1349,7 +1394,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" @@ -1367,7 +1412,7 @@ "message": "Export type" }, "accountRestricted": { - "message": "Account restricted" + "message": "Konto begrenset" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { "message": "“File password” and “Confirm file password“ do not match." @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Det nye hovedpassordet ditt oppfyller ikke vilkårene." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "Biometri i nettleserutvidelsen krever først aktivering i innstillinger i skrivebordsprogrammet." }, + "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": "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." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "E-postbekreftelse kreves" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Du må bekrefte E-postadressen din for å bruke denne funksjonen." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Tidsavbruddet ditt for hvelvet overstiger begrensningene som er satt av organisasjonen din." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatisk registrering" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Lær mer om importalternativene dine" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index 560a5c41c70..113d90d32a6 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Settings" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Your login session has expired." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a premium member!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,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 organization and choose from available collections." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index 796b23b0c36..7936d15b960 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" @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Hoofdwachtwoord" + }, + "masterPassImportant": { + "message": "Je kunt je hoofdwachtwoord niet herstellen als je het vergeet!" + }, + "confirmMasterPassword": { + "message": "Hoofdwachtwoord bevestigen" + }, + "masterPassHintLabel": { + "message": "Hoofdwachtwoordhint" + }, + "joinOrganization": { + "message": "Join organisatie" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Voltooi je lidmaatschap aan deze organisatie door een hoofdwachtwoord in te stellen." + }, "settings": { "message": "Instellingen" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Je nieuwe account is aangemaakt! Je kunt nu inloggen." }, + "newAccountCreated2": { + "message": "Je nieuwe account is aangemaakt!" + }, + "youHaveBeenLoggedIn": { + "message": "Je bent ingelogd!" + }, "masterPassSent": { "message": "We hebben je een e-mail gestuurd met je hoofdwachtwoordhint." }, @@ -592,16 +616,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" @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Verificatiecode vereist." }, + "webauthnCancelOrTimeout": { + "message": "De authenticatie werd geannuleerd of duurde te lang. Probeer het opnieuw." + }, "invalidVerificationCode": { "message": "Ongeldige verificatiecode" }, @@ -677,7 +704,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": { @@ -777,6 +804,18 @@ "loginExpired": { "message": "Je inlogsessie is verlopen." }, + "restartRegistration": { + "message": "Registratie herstarten" + }, + "expiredLink": { + "message": "Verlopen link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Herstart de registratie of probeer in te loggen." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Je hebt al een account" + }, "logOutConfirmation": { "message": "Weet je zeker dat je wilt uitloggen?" }, @@ -814,7 +853,7 @@ "message": "Hulp en feedback" }, "getHelp": { - "message": "Hulp vragen" + "message": "Vraag ondersteuning" }, "fileBugReport": { "message": "Rapporteer een fout (bug)" @@ -849,10 +888,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" @@ -946,7 +985,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": { @@ -954,7 +993,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." @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Je kunt een Premium-abonnement aanschaffen in de webkluis op bitwarden.com. Wil je de website nu bezoeken?" }, + "premiumPurchaseAlertV2": { + "message": "Je kunt Premium via je accountinstellingen in de Bitwarden-webapp kopen." + }, "premiumCurrentMember": { "message": "Je bent Premium-lid!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Fout bij vernieuwen toegangstoken" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Je nieuwe hoofdwachtwoord voldoet niet aan de beleidseisen." }, - "receiveMarketingEmails": { - "message": "Ontvang e-mailberichten van Bitwarden voor aankondigingen, advies en onderzoeksmogelijkheden." + "receiveMarketingEmailsV2": { + "message": "Krijg advies, aankondigingen en onderzoeksmogelijkheden van Bitwarden in je inbox." }, "unsubscribe": { "message": "Afmelden" @@ -1771,6 +1825,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." }, @@ -1944,7 +2004,7 @@ "message": "Wordt verwijderd" }, "webAuthnAuthenticate": { - "message": "Authenticeer WebAuthn" + "message": "Verifieer WebAuthn" }, "hideEmail": { "message": "Verberg mijn e-mailadres voor ontvangers." @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "E-mailverificatie vereist" }, + "emailVerifiedV2": { + "message": "E-mailadres geverifieerd" + }, "emailVerificationRequiredDesc": { "message": "Je moet je e-mailadres verifiëren om deze functionaliteit te gebruiken." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Je kluis time-out is hoger dan het maximum van jouw organisatie." }, + "inviteAccepted": { + "message": "Uitnodiging geaccepteerd" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatische inschrijving" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Fout bij het verbinden met de Duo-service. Gebruik een andere tweestapsaanmeldingsmethode of neem contact op met Duo voor hulp." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Start Duo en volg de stappen om in te loggen." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Onjuist bestandswachtwoord, gebruik het wachtwoord dat je hebt ingevoerd bij het aanmaken van het exportbestand." }, - "importDestination": { - "message": "Importbestemming" + "destination": { + "message": "Bestemming" }, "learnAboutImportOptions": { "message": "Leer meer over je importopties" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Gegevens" + }, + "fileSends": { + "message": "Bestand-Sends" + }, + "textSends": { + "message": "Tekst-Sends" + }, + "ssoError": { + "message": "Er zijn geen vrije poorten gevonden voor de sso-login." + }, + "fileSavedToDevice": { + "message": "Bestand op apparaat opgeslagen. Beheer vanaf de downloads op je apparaat." } } diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index f6d4a6ed3c4..558cc4bbc79 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Innstillingar" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Den nye kontoen din har blitt oppretta! Du kan no logge inn." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "Me har sendt deg ein e-post med eit hint om hovudpassordet ditt." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Ein stadfestingskode er påkravt." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Ugyldig stadfestingskode" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Innloggingsøkta di har gått ut." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Er du sikker på at du vil logge ut?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Du kan kjøpe eit Premium-medlemsskap i Bitwarden sin nettkvelv. Vil du gå til nettstaden no?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Du er eit Premium-medlem!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,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 organization and choose from available collections." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index d6781321446..0a1e42c2253 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Settings" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Your login session has expired." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a premium member!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,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 organization and choose from available collections." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index ead83d7fa7c..ef352115ba1 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Hasło główne" + }, + "masterPassImportant": { + "message": "Twoje hasło główne nie może zostać odzyskane, jeśli je zapomnisz!" + }, + "confirmMasterPassword": { + "message": "Potwierdź hasło główne" + }, + "masterPassHintLabel": { + "message": "Podpowiedź do hasła głównego" + }, + "joinOrganization": { + "message": "Dołącz do organizacji" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Zakończ dołączanie do tej organizacji przez ustawienie hasła głównego." + }, "settings": { "message": "Ustawienia" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Konto zostało utworzone! Teraz możesz się zalogować." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "Wysłaliśmy Tobie wiadomość e-mail z podpowiedzią do hasła głównego." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Kod weryfikacyjny jest wymagany." }, + "webauthnCancelOrTimeout": { + "message": "Uwierzytelnianie zostało anulowane lub trwało zbyt długo. Spróbuj ponownie." + }, "invalidVerificationCode": { "message": "Kod weryfikacyjny jest nieprawidłowy" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Twoja sesja wygasła." }, + "restartRegistration": { + "message": "Zrestartuj rejestrację" + }, + "expiredLink": { + "message": "Link wygasł" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Zrestartuj rejestrację lub spróbuj się zalogować." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Możesz mieć już konto" + }, "logOutConfirmation": { "message": "Czy na pewno chcesz się wylogować?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Konto Premium możesz zakupić na stronie sejfu bitwarden.com. Czy chcesz otworzyć tę stronę?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Posiadasz konto Premium!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Błąd podczas odświeżania tokenu" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Nowe hasło główne nie spełnia wymaganych zasad." }, - "receiveMarketingEmails": { - "message": "Otrzymuj e-maile od Bitwarden z ogłoszeniami, poradami i badaniami." + "receiveMarketingEmailsV2": { + "message": "Uzyskaj poradę, ogłoszenia i możliwości badawcze od Bitwarden w swojej skrzynce odbiorczej." }, "unsubscribe": { "message": "Anuluj subskrypcję" @@ -1771,6 +1825,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." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Weryfikacja adresu e-mail jest wymagana" }, + "emailVerifiedV2": { + "message": "E-mail zweryfikowany" + }, "emailVerificationRequiredDesc": { "message": "Musisz zweryfikować adres e-mail, aby używać tej funkcji." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Czas blokowania sejfu przekracza limit określony przez organizację." }, + "inviteAccepted": { + "message": "Zaproszenie zostało zaakceptowane" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatyczne rejestrowanie użytkowników" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Wystąpił błąd podczas połączenia z usługą Duo. Aby uzyskać pomoc, użyj innej metody dwustopniowego logowania lub skontaktuj się z Duo." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Uruchom Duo i wykonaj kroki, aby zakończyć logowanie." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Hasło do pliku jest nieprawidłowe. Użyj hasła które podano przy tworzeniu pliku eksportu." }, - "importDestination": { - "message": "Miejsce docelowe importu" + "destination": { + "message": "Miejsce docelowe" }, "learnAboutImportOptions": { "message": "Dowiedz się więcej o opcjach importu" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Dane" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index de5ed3fc600..f2ce6154299 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", @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Senha mestra" + }, + "masterPassImportant": { + "message": "Sua senha mestra não pode ser recuperada se você a esquecer!" + }, + "confirmMasterPassword": { + "message": "Confirme a senha mestra" + }, + "masterPassHintLabel": { + "message": "Dica da senha mestra" + }, + "joinOrganization": { + "message": "Juntar-se à organização" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Termine de juntar-se nessa organização definindo uma senha mestra." + }, "settings": { "message": "Configurações" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "A sua nova conta foi criada! Agora você pode iniciar a sessão." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "Enviamos um e-mail com a dica da sua senha mestra." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Requer o código de verificação." }, + "webauthnCancelOrTimeout": { + "message": "A autenticação foi cancelada ou demorou muito. Por favor tente novamente." + }, "invalidVerificationCode": { "message": "Código de verificação inválido" }, @@ -667,17 +694,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": { @@ -694,7 +721,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" @@ -777,6 +804,18 @@ "loginExpired": { "message": "A sua sessão expirou." }, + "restartRegistration": { + "message": "Reiniciar registro" + }, + "expiredLink": { + "message": "Link expirado" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Por favor, reinicie o registro ou tente fazer login." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Você pode já ter uma conta" + }, "logOutConfirmation": { "message": "Você tem certeza que deseja sair?" }, @@ -946,7 +985,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": { @@ -960,7 +999,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." @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Você pode comprar a assinatura premium no cofre web em bitwarden.com. Você deseja visitar o site agora?" }, + "premiumPurchaseAlertV2": { + "message": "Você pode comprar Premium nas configurações de sua conta no aplicativo web do Bitwarden." + }, "premiumCurrentMember": { "message": "Você é um membro premium!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Erro ao Atualizar Token" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "A sua nova senha mestra não cumpre aos requisitos da política." }, - "receiveMarketingEmails": { - "message": "Obtenha e-mails do Bitwarden para anúncios, conselhos e oportunidades de pesquisa." + "receiveMarketingEmailsV2": { + "message": "Obtenha conselhos, novidades, e oportunidades de pesquisa do Bitwarden em sua caixa de entrada." }, "unsubscribe": { "message": "Cancelar subscrição" @@ -1769,7 +1823,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." @@ -1833,7 +1893,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": { @@ -1849,7 +1909,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": { @@ -1857,26 +1917,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": { @@ -1912,7 +1972,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": { @@ -1953,7 +2013,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": "E-mail verificado" }, "emailVerificationRequiredDesc": { "message": "Você precisa verificar o seu e-mail para usar este recurso." @@ -1968,7 +2031,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" @@ -1979,6 +2042,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" }, @@ -2022,7 +2088,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", @@ -2063,8 +2129,11 @@ "vaultTimeoutTooLarge": { "message": "O tempo limite do seu cofre excede as restrições definidas por sua organização." }, + "inviteAccepted": { + "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." @@ -2178,23 +2247,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" @@ -2206,7 +2275,7 @@ "message": "Aleatório" }, "randomWord": { - "message": "Palavra Aleatória" + "message": "Palavra aleatória" }, "websiteName": { "message": "Nome do site" @@ -2706,7 +2775,7 @@ "message": "Submenu" }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "Ativar/desativar navegação lateral" }, "skipToContent": { "message": "Ir para o conteúdo" @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "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." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Senha do arquivo inválida, por favor informe a senha utilizada quando criou o arquivo de exportação." }, - "importDestination": { - "message": "Destino da Importação" + "destination": { + "message": "Destino" }, "learnAboutImportOptions": { "message": "Saiba mais sobre suas opções de importação" @@ -2844,7 +2916,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" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Dado" + }, + "fileSends": { + "message": "Arquivos enviados" + }, + "textSends": { + "message": "Texto enviado" + }, + "ssoError": { + "message": "Nenhuma porta livre foi encontrada para o cliente final." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index a1b9d5d6238..a92f4c54b47 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -346,7 +346,7 @@ "message": "Remover" }, "nameRequired": { - "message": "É necessário o nome." + "message": "O nome é obrigatório." }, "addedItem": { "message": "Item adicionado" @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Palavra-passe mestra" + }, + "masterPassImportant": { + "message": "A sua palavra-passe mestra não pode ser recuperada se a esquecer!" + }, + "confirmMasterPassword": { + "message": "Confirmar a palavra-passe mestra" + }, + "masterPassHintLabel": { + "message": "Dica da palavra-passe mestra" + }, + "joinOrganization": { + "message": "Aderir à organização" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Conclua a adesão a esta organização ao definir uma palavra-passe mestra." + }, "settings": { "message": "Definições" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "A sua nova conta foi criada! Pode agora iniciar sessão." }, + "newAccountCreated2": { + "message": "A sua nova conta foi criada!" + }, + "youHaveBeenLoggedIn": { + "message": "Iniciou sessão!" + }, "masterPassSent": { "message": "Enviámos-lhe um e-mail com a dica da sua palavra-passe mestra." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "É necessário o código de verificação." }, + "webauthnCancelOrTimeout": { + "message": "A autenticação foi cancelada ou demorou demasiado tempo. Por favor, tente novamente." + }, "invalidVerificationCode": { "message": "Código de verificação inválido" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "A sua sessão expirou." }, + "restartRegistration": { + "message": "Reiniciar registo" + }, + "expiredLink": { + "message": "Link expirado" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Por favor, reinicie o registo ou tente iniciar sessão." + }, + "youMayAlreadyHaveAnAccount": { + "message": "É possível que já tenha uma conta" + }, "logOutConfirmation": { "message": "Tem a certeza de que pretende terminar sessão?" }, @@ -846,7 +885,7 @@ "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." }, "goToWebVault": { - "message": "Ir para o cofre web" + "message": "Ir para o cofre Web" }, "getMobileApp": { "message": "Obter a aplicação móvel" @@ -1029,7 +1068,7 @@ "message": "Tema" }, "themeDesc": { - "message": "Alterar o tema de cores da aplicação." + "message": "Altere o tema de cores da aplicação." }, "dark": { "message": "Escuro", @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Pode adquirir uma subscrição Premium no cofre web em bitwarden.com. Pretende visitar o site agora?" }, + "premiumPurchaseAlertV2": { + "message": "Pode adquirir o Premium a partir das definições da sua conta na aplicação Web do Bitwarden." + }, "premiumCurrentMember": { "message": "É um membro Premium!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Erro no acesso ao token de atualização" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "A sua nova palavra-passe mestra não cumpre os requisitos da política." }, - "receiveMarketingEmails": { - "message": "Receba e-mails do Bitwarden com anúncios, conselhos e oportunidades de investigação." + "receiveMarketingEmailsV2": { + "message": "Receba conselhos, anúncios e oportunidades de investigação do Bitwarden na sua caixa de entrada." }, "unsubscribe": { "message": "Anular subscrição" @@ -1771,6 +1825,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." }, @@ -1944,7 +2004,7 @@ "message": "Eliminação pendente" }, "webAuthnAuthenticate": { - "message": "Autenticar WebAuthn" + "message": "Autenticar o WebAuthn" }, "hideEmail": { "message": "Ocultar o meu endereço de e-mail dos destinatários." @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Verificação de e-mail necessária" }, + "emailVerifiedV2": { + "message": "E-mail verificado" + }, "emailVerificationRequiredDesc": { "message": "É necessário verificar o seu e-mail para utilizar esta funcionalidade." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "O tempo limite do seu cofre excede as restrições definidas pela sua organização." }, + "inviteAccepted": { + "message": "Convite aceite" + }, "resetPasswordPolicyAutoEnroll": { "message": "Inscrição automática" }, @@ -2094,10 +2163,10 @@ } }, "leaveOrganization": { - "message": "Deixar a organização" + "message": "Sair da organização" }, "leaveOrganizationConfirmation": { - "message": "Tem a certeza de que pretende deixar esta organização?" + "message": "Tem a certeza de que pretende sair desta organização?" }, "leftOrganization": { "message": "Saiu da organização." @@ -2606,10 +2675,10 @@ "message": "Dispositivo de confiança" }, "inputRequired": { - "message": "Campo necessário." + "message": "Campo obrigatório." }, "required": { - "message": "necessário" + "message": "obrigatório" }, "search": { "message": "Procurar" @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Erro ao ligar ao serviço Duo. Utilize um método de verificação de dois passos diferente ou contacte o Duo para obter assistência." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Inicie o Duo e siga os passos para concluir o início de sessão." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Palavra-passe de ficheiro inválida, utilize a palavra-passe que introduziu quando criou o ficheiro de exportação." }, - "importDestination": { - "message": "Destino da importação" + "destination": { + "message": "Destino" }, "learnAboutImportOptions": { "message": "Saiba mais sobre as suas opções de importação" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Dados" + }, + "fileSends": { + "message": "Sends de ficheiros" + }, + "textSends": { + "message": "Sends de texto" + }, + "ssoError": { + "message": "Não foi possível encontrar portas livres para o início de sessão sso." + }, + "fileSavedToDevice": { + "message": "Ficheiro guardado no dispositivo. Gira-o a partir das transferências do seu dispositivo." } } diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 874240889ed..0ba398daa80 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Setări" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Noul dvs. cont a fost creat! Acum vă puteți autentifica." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "V-am trimis un e-mail cu indiciul parolei principale." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Este necesar codul de verificare." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Cod de verificare nevalid" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Sesiunea de autentificare a expirat." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Sigur doriți să vă deconectați?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Puteți achiziționa un abonament premium pe saitul web bitwarden.com. Doriți să vizitați saitul acum?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Sunteți un membru premium!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Noua dvs. parolă principală nu îndeplinește cerințele politicii." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "Biometria browserului necesită ca mai întâi să fie configurată biometria desktopului în setări." }, + "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": "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." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Verificare e-mail necesară" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Trebuie să vă verificați e-mailul pentru a utiliza această caracteristică." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Timpul de expirare al seifului depășește restricțiile stabilite de organizația dvs." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Înscriere automată" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 92330eeb3aa..2a23e96c5f6 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Мастер-пароль" + }, + "masterPassImportant": { + "message": "Ваш мастер-пароль невозможно восстановить, если вы его забудете!" + }, + "confirmMasterPassword": { + "message": "Подтвердите мастер-пароль" + }, + "masterPassHintLabel": { + "message": "Подсказка к мастер-паролю" + }, + "joinOrganization": { + "message": "Присоединиться к организации" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Завершите присоединение к этой организации, установив мастер-пароль." + }, "settings": { "message": "Настройки" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Аккаунт создан! Теперь вы можете войти в систему." }, + "newAccountCreated2": { + "message": "Ваш новый аккаунт создан!" + }, + "youHaveBeenLoggedIn": { + "message": "Вы авторизовались!" + }, "masterPassSent": { "message": "Мы отправили вам письмо с подсказкой к мастер-паролю." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Необходим код подтверждения." }, + "webauthnCancelOrTimeout": { + "message": "Аутентификация была отменена или заняла слишком много времени. Пожалуйста, попробуйте еще раз." + }, "invalidVerificationCode": { "message": "Неверный код подтверждения" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Истек срок действия вашего сеанса." }, + "restartRegistration": { + "message": "Перезапустить регистрацию" + }, + "expiredLink": { + "message": "Истекшая ссылка" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Пожалуйста, перезапустите регистрацию или попробуйте авторизоваться." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Возможно, у вас уже есть аккаунт" + }, "logOutConfirmation": { "message": "Вы действительно хотите выйти?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Вы можете купить Премиум на bitwarden.com. Перейти на сайт сейчас?" }, + "premiumPurchaseAlertV2": { + "message": "Премиум можно приобрести в настройках аккаунта в веб-версии Bitwarden." + }, "premiumCurrentMember": { "message": "У вас есть Премиум!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Скопировано успешно" + }, "errorRefreshingAccessToken": { "message": "Ошибка обновления токена доступа" }, @@ -1477,9 +1522,15 @@ "additionalWindowsHelloSettings": { "message": "Дополнительные настройки Windows Hello" }, + "unlockWithPolkit": { + "message": "Разблокировать с помощью системной аутентификации" + }, "windowsHelloConsentMessage": { "message": "Верификация для Bitwarden." }, + "polkitConsentMessage": { + "message": "Для разблокировки Bitwarden пройдите аутентификацию." + }, "unlockWithTouchId": { "message": "Разблокировать с Touch ID" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "Запрашивать Windows Hello при запуске приложения" }, + "autoPromptPolkit": { + "message": "Запрашивать системную аутентификацию при запуске" + }, "autoPromptTouchId": { "message": "Запрашивать Touch ID при запуске приложения" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Ваш новый мастер-пароль не соответствует требованиям политики." }, - "receiveMarketingEmails": { - "message": "Получайте электронные письма от Bitwarden с анонсами, советами и возможностями для исследований." + "receiveMarketingEmailsV2": { + "message": "Получайте советы, анонсы и возможности для исследований от Bitwarden на свой email." }, "unsubscribe": { "message": "Отписаться" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "Для активации биометрии в браузере сначала необходимо включить биометрию в приложении для компьютера." }, + "biometricsManualSetupTitle": { + "message": "Автоматическая настройка недоступна" + }, + "biometricsManualSetupDesc": { + "message": "Из-за метода инсталляции не удалось автоматически включить поддержку биометрии. Вы хотите открыть документацию чтобы узнать, как это сделать вручную?" + }, "personalOwnershipSubmitError": { "message": "В соответствии с корпоративной политикой вам запрещено сохранять элементы в личном хранилище. Измените владельца на организацию и выберите из доступных Коллекций." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Требуется подтверждение электронной почты" }, + "emailVerifiedV2": { + "message": "Email подтвержден" + }, "emailVerificationRequiredDesc": { "message": "Для использования этой функции необходимо подтвердить свою электронную почту." }, @@ -1979,6 +2042,9 @@ "updateWeakMasterPasswordWarning": { "message": "Ваш мастер-пароль не соответствует требованиям политики вашей организации. Для доступа к хранилищу вы должны обновить свой мастер-пароль прямо сейчас. При этом текущий сеанс будет завершен и потребуется повторная авторизация. Сеансы на других устройствах могут оставаться активными в течение часа." }, + "tdeDisabledMasterPasswordRequired": { + "message": "В вашей организации отключено шифрование доверенных устройств. Пожалуйста, установите мастер-пароль для доступа к вашему хранилищу." + }, "tryAgain": { "message": "Попробуйте снова" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Тайм-аут вашего хранилища превышает ограничения, установленные вашей организацией." }, + "inviteAccepted": { + "message": "Приглашение принято" + }, "resetPasswordPolicyAutoEnroll": { "message": "Автоматическое развертывание" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Ошибка при подключении к сервису Duo. Используйте другой метод двухэтапной аутентификации или обратитесь за помощью в Duo." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Запустите Duo и следуйте шагам для завершения авторизации." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Неверный пароль к файлу. Используйте пароль, введенный при создании файла экспорта." }, - "importDestination": { - "message": "Цель импорта" + "destination": { + "message": "Назначение" }, "learnAboutImportOptions": { "message": "Узнайте о возможностях импорта" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Данные" + }, + "fileSends": { + "message": "Файловая Send" + }, + "textSends": { + "message": "Текстовая Send" + }, + "ssoError": { + "message": "Не удалось найти свободные порты для авторизации SSO." + }, + "fileSavedToDevice": { + "message": "Файл сохранен на устройстве. Управляйте им из загрузок устройства." } } diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 906181c7bdb..c310cbf9c05 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "සැකසුම්" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Your login session has expired." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a premium member!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,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 organization and choose from available collections." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 42ebe6965b3..516c13cc86b 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -95,7 +95,7 @@ "message": "Heslo" }, "passphrase": { - "message": "Heslo" + "message": "Prístupová fráza" }, "editItem": { "message": "Upraviť položku" @@ -367,7 +367,7 @@ "message": "Naozaj chcete odstrániť túto položku?" }, "deletedItem": { - "message": "Položka odstránená" + "message": "Položka bola presunutá do koša" }, "overwritePasswordConfirmation": { "message": "Naozaj chcete prepísať aktuálne heslo?" @@ -445,13 +445,13 @@ "message": "Vyhnúť sa zameniteľným znakom" }, "searchCollection": { - "message": "Vyhľadať zbierku" + "message": "Hľadať v zbierke" }, "searchFolder": { - "message": "Search folder" + "message": "Hľadať v priečinku" }, "searchFavorites": { - "message": "Search favorites" + "message": "Hľadať v obľúbených" }, "searchType": { "message": "Search type", @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Hlavné heslo" + }, + "masterPassImportant": { + "message": "Vaše hlavné heslo sa nebude dať obnoviť, ak ho zabudnete!" + }, + "confirmMasterPassword": { + "message": "Potvrdiť hlavné heslo" + }, + "masterPassHintLabel": { + "message": "Nápoveda pre hlavné heslo" + }, + "joinOrganization": { + "message": "Pripojte sa k organizácii" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Dokončite pripojenie k tejto organizácii nastavením hlavného hesla." + }, "settings": { "message": "Nastavenia" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Váš nový účet bol vytvorený! Teraz sa môžete prihlásiť." }, + "newAccountCreated2": { + "message": "Váš nový účet bol vytvorený!" + }, + "youHaveBeenLoggedIn": { + "message": "Boli ste prihlásený!" + }, "masterPassSent": { "message": "Emailom sme vám poslali nápoveď k hlavnému heslu." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Overovací kód je povinný." }, + "webauthnCancelOrTimeout": { + "message": "Overenie bolo zrušené alebo trvalo príliš dlho. Skúste to znova." + }, "invalidVerificationCode": { "message": "Neplatný verifikačný kód" }, @@ -674,7 +701,7 @@ "message": "Bezpečnostný kľúč Yubico OTP" }, "yubiKeyDesc": { - "message": "Použiť YubiKey pre prístup k vášmu účtu. Pracuje s YubiKey 4, 4 Nano, 4C a s NEO zariadeniami." + "message": "Použiť YubiKey na prístup k vášmu účtu. Pracuje s YubiKey 4, 4 Nano, 4C a s NEO zariadeniami." }, "duoDescV2": { "message": "Zadajte kód vygenerovaný aplikáciou Duo Security.", @@ -745,7 +772,7 @@ "message": "URL adresa servera pre oznámenia" }, "iconsUrl": { - "message": "URL servera ikôn" + "message": "URL servera ikon" }, "environmentSaved": { "message": "URL prostredia boli uložené." @@ -777,6 +804,18 @@ "loginExpired": { "message": "Platnosť prihlásenia vypršala." }, + "restartRegistration": { + "message": "Zopakovať registráciu" + }, + "expiredLink": { + "message": "Platnosť odkazu vypršala" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Prosím, zopakujte registráciu alebo sa pokúste prihlásiť." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Možno už máte účet" + }, "logOutConfirmation": { "message": "Naozaj sa chcete odhlásiť?" }, @@ -1059,7 +1098,7 @@ "message": "Reštartovať pre dokončenie aktualizácie" }, "restartToUpdateDesc": { - "message": "Verzia $VERSION_NUM$ je pripravená na inštaláciu. Je nutné reštartovať aplikáciu, aby sa inštalácia mohla dokončiť. Chcete ju reŝtartovať a aktualizovať teraz?", + "message": "Verzia $VERSION_NUM$ je pripravená na inštaláciu. Je nutné reštartovať aplikáciu, aby sa inštalácia mohla dokončiť. Chcete ju reštartovať a aktualizovať teraz?", "placeholders": { "version_num": { "content": "$1", @@ -1141,11 +1180,14 @@ "premiumPurchaseAlert": { "message": "Svoje prémiové členstvo môžete zakúpiť vo webovom trezore bitwarden.com. Chcete navštíviť túto stránku teraz?" }, + "premiumPurchaseAlertV2": { + "message": "Prémiové členstvo si môžete zakúpiť v nastaveniach svojho účtu vo webovej aplikácii Bitwarden." + }, "premiumCurrentMember": { "message": "Ste prémiovým členom!" }, "premiumCurrentMemberThanks": { - "message": "Ďakujeme za podporu Bitwarden." + "message": "Ďakujeme za podporu Bitwardenu." }, "premiumPrice": { "message": "Všetko len za $PRICE$/rok!", @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Úspešne skopírované" + }, "errorRefreshingAccessToken": { "message": "Chyba obnovenia prístupového tokenu" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Vaše nové hlavné heslo nespĺňa pravidlá." }, - "receiveMarketingEmails": { - "message": "Dostávať e-maily od Bitwardenu s oznámeniami, radami a možnosťami výskumu." + "receiveMarketingEmailsV2": { + "message": "Dostávajte do schránky rady, oznámenia a príležitosti na výskum od spoločnosti Bitwarden." }, "unsubscribe": { "message": "Odhlásiť sa z odberu" @@ -1771,6 +1825,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." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Vyžaduje sa overenie e-mailu" }, + "emailVerifiedV2": { + "message": "Overený e-mail" + }, "emailVerificationRequiredDesc": { "message": "Na používanie tejto funkcie musíte overiť svoj e-mail." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Časový limit vášho trezora prekračuje obmedzenia nastavené vašou organizáciou." }, + "inviteAccepted": { + "message": "Pozvánka prijatá" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatická registrácia" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Chyba pri pripájaní k službe Duo. Použite inú metódu dvojstupňového prihlásenia alebo kontaktujte Duo a požiadajte o pomoc." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Spustite DuO a postupujte podľa pokynov na dokončenie prihlásenia." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Neplatné heslo súboru, použite heslo, ktoré ste zadali pri vytváraní exportného súboru." }, - "importDestination": { - "message": "Cieľ importu" + "destination": { + "message": "Cieľ" }, "learnAboutImportOptions": { "message": "Zistiť viac o možnostiach importu" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Údaje" + }, + "fileSends": { + "message": "Sendy so súborom" + }, + "textSends": { + "message": "Textové Sendy" + }, + "ssoError": { + "message": "Pre prihlásenie SSO sa nepodarilo nájsť žiadne voľné porty." + }, + "fileSavedToDevice": { + "message": "Súbor sa uložil do zariadenia. Spravujte stiahnuté súbory zo zariadenia." } } diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index e275b396620..28bbd3b483e 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Nastavitve" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Potrebna je verifikacijska koda." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Neveljavna verifikacijska koda" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Vaša seja je potekla." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a premium member!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,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 organization and choose from available collections." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index e254b694c34..39e4a1015f2 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Главна лозинка" + }, + "masterPassImportant": { + "message": "Ваша главна лозинка се не може повратити ако је заборавите!" + }, + "confirmMasterPassword": { + "message": "Потрдити главну лозинку" + }, + "masterPassHintLabel": { + "message": "Савет главне лозинке" + }, + "joinOrganization": { + "message": "Придружи Организацију" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Завршите придруживање овој организацији постављањем главне лозинке." + }, "settings": { "message": "Подешавања" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Ваш налог је креиран! Сада се можете пријавити." }, + "newAccountCreated2": { + "message": "Ваш нови налог је направљен!" + }, + "youHaveBeenLoggedIn": { + "message": "Пријављени сте!" + }, "masterPassSent": { "message": "Послали смо Вам поруку са саветом главне лозинке." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Верификациони код је обавезан." }, + "webauthnCancelOrTimeout": { + "message": "Аутентификација је отказана или је трајала предуго. Молим вас, покушајте поново." + }, "invalidVerificationCode": { "message": "Неисправан верификациони код" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Ваша сесија је истекла." }, + "restartRegistration": { + "message": "Поново покрените регистрацију" + }, + "expiredLink": { + "message": "Истекла веза" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Поново покрените регистрацију или покушајте да се пријавите." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Можда већ имате налог" + }, "logOutConfirmation": { "message": "Заиста желите да се одјавите?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Можете купити премијум претплату на bitwarden.com. Да ли желите да посетите веб сајт сада?" }, + "premiumPurchaseAlertV2": { + "message": "Можете да купите Премиум у подешавањима налога у веб апликацији Bitwarden." + }, "premiumCurrentMember": { "message": "Ви сте премијум члан!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Грешка при освежавању токена приступа" }, @@ -1477,9 +1522,15 @@ "additionalWindowsHelloSettings": { "message": "Додатна подешавања Windows Hello" }, + "unlockWithPolkit": { + "message": "Откључати системском аутентификацијом" + }, "windowsHelloConsentMessage": { "message": "Потврди за Bitwarden." }, + "polkitConsentMessage": { + "message": "Аутентификујте се да бисте откључали Bitwarden." + }, "unlockWithTouchId": { "message": "Откључај са Touch ID" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "Захтевај Windows Hello при покретању" }, + "autoPromptPolkit": { + "message": "Затражите аутентификацију система при покретању" + }, "autoPromptTouchId": { "message": "Захтевај Touch ID при покретању" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Ваша нова главна лозинка не испуњава захтеве полисе." }, - "receiveMarketingEmails": { - "message": "Добијајте е-пошту од Bitwarden-а за најаве, савете и могућности истраживања." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Одјави се" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "Биометрија прегледача захтева да у поставкама прво буде омогућена биометрија desktop-а." }, + "biometricsManualSetupTitle": { + "message": "Аутоматско подешавање није доступно" + }, + "biometricsManualSetupDesc": { + "message": "Због начина инсталације, подршка за биометрију није могла бити аутоматски омогућена. Да ли желите да отворите документацију о томе како то да ручно урадите?" + }, "personalOwnershipSubmitError": { "message": "Због смерница за предузећа, ограничено вам је чување предмета у вашем личном трезору. Промените опцију власништва у организацију и изаберите из доступних колекција." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Потребна је верификација е-поште" }, + "emailVerifiedV2": { + "message": "Имејл верификован" + }, "emailVerificationRequiredDesc": { "message": "Морате да проверите е-пошту да бисте користили ову функцију." }, @@ -1979,6 +2042,9 @@ "updateWeakMasterPasswordWarning": { "message": "Ваша главна лозинка не испуњава једну или више смерница ваше организације. Да бисте приступили сефу, морате одмах да ажурирате главну лозинку. Ако наставите, одјавићете се са ваше тренутне сесије, што захтева да се поново пријавите. Активне сесије на другим уређајима могу да остану активне до један сат." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Ваша организација је онемогућила шифровање поузданог уређаја. Поставите главну лозинку за приступ вашем трезору." + }, "tryAgain": { "message": "Покушајте поново" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Време истека вашег сефа је премашило дозвољена ограничења од стране ваше организације." }, + "inviteAccepted": { + "message": "Позив прихваћен" + }, "resetPasswordPolicyAutoEnroll": { "message": "Аутоматска пријава" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Грешка при повезивању са услугом Duo. Користите други метод пријаве у два корака или контактирајте Duo за помоћ." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Покренути Duo и пратите кораке да бисте завршили пријављивање." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Неважећа лозинка за датотеку, користите лозинку коју сте унели када сте креирали датотеку за извоз." }, - "importDestination": { - "message": "Смештај увоза" + "destination": { + "message": "Одредиште" }, "learnAboutImportOptions": { "message": "Сазнајте више о опцијама увоза" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Подаци" + }, + "fileSends": { + "message": "Датотека „Send“" + }, + "textSends": { + "message": "Текст „Send“" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index 3e73b2598b7..dea1a265714 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Huvudlösenord" + }, + "masterPassImportant": { + "message": "Ditt huvudlösenord kan inte återställas om du glömmer det!" + }, + "confirmMasterPassword": { + "message": "Bekräfta huvudlösenord" + }, + "masterPassHintLabel": { + "message": "Huvudlösenordsledtråd" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Inställningar" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Ditt nya konto har skapats! Du kan nu logga in." }, + "newAccountCreated2": { + "message": "Ditt nya konto har skapats!" + }, + "youHaveBeenLoggedIn": { + "message": "Du har blivit inloggad!" + }, "masterPassSent": { "message": "Vi har skickat ett e-postmeddelande till dig med din huvudlösenordsledtråd." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Verifieringskod krävs." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Ogiltig verifieringskod" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Din inloggningssession har löpt ut." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Är du säker på att du vill logga ut?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Du kan köpa premium-medlemskap i Bitwardens webbvalv. Vill du besöka webbplatsen nu?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "Du är en premium-medlem!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Ditt nya huvudlösenord uppfyller inte kraven i policyn." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "Biometri i webbläsaren kräver att biometri på skrivbordet aktiveras i inställningarna först." }, + "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": "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." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "E-postverifiering krävs" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "Du måste verifiera din e-post för att använda den här funktionen." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Ditt valvs tid för timeout överskrider de begränsningar som fastställts av din organisation." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatiskt deltagande" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Starta Duo och följ stegen för att slutföra inloggningen." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Ogiltigt fillösenord, använd lösenordet du angav när du skapade exportfilen." }, - "importDestination": { - "message": "Importdestination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Läs mer om dina importalternativ" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 0887d769823..9194fd7c22a 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "Settings" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Your new account has been created! You may now log in." }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Verification code is required." }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "Invalid verification code" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "Your login session has expired." }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "Are you sure you want to log out?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a premium member!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,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 organization and choose from available collections." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index 8c0a06ec424..8a08fd5a993 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -239,7 +239,7 @@ "message": "นางสาว" }, "mx": { - "message": "Mx" + "message": "เอมเอ็ก" }, "dr": { "message": "ดร." @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "การตั้งค่า" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "บัญชีใหม่ของคุณถูกสร้างขึ้นแล้ว! ตอนนี้คุณสามารถเข้าสู่ระบบ" }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "เราได้ส่งอีเมลพร้อมคำใบ้รหัสผ่านหลักของคุณ" }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "จำเป็นต้องมีรหัสการตรวจสอบยืนยัน" }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "รหัสการตรวจสอบสิทธิ์ไม่ถูกต้อง" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "เซสชันของคุณหมดอายุแล้ว" }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "คุณแน่ใจว่าคุณต้องการที่จะออกจากระบบ?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "You can purchase premium membership on the bitwarden.com web vault. Do you want to visit the website now?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "You are a premium member!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1477,9 +1522,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" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Your new master password does not meet the policy requirements." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,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 organization and choose from available collections." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Your vault timeout exceeds the restrictions set by your organization." }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic enrollment" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Invalid file password, please use the password you entered when you created the export file." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "Learn about your import options" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index b44135e8d43..49d885f45b3 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -527,7 +527,7 @@ "message": "Ana parola ipucu (isteğe bağlı)" }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Parolanızı unutursanız parola ipucunuzu e-posta adresinize gönderebiliriz. Maksimum $CURRENT$/$MAXIMUM$ karakter.", "placeholders": { "current": { "content": "$1", @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Ana parola" + }, + "masterPassImportant": { + "message": "Ana parolanızı unutursanız kurtaramazsınız!" + }, + "confirmMasterPassword": { + "message": "Ana parolayı onaylayın" + }, + "masterPassHintLabel": { + "message": "Ana parola ipucu" + }, + "joinOrganization": { + "message": "Kuruluşa katıl" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Kuruluşa katılmayı tamamlamak için ana parolanızı belirleyin." + }, "settings": { "message": "Ayarlar" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Yeni hesabınız oluşturuldu! Şimdi giriş yapabilirsiniz." }, + "newAccountCreated2": { + "message": "Yeni hesabınız oluşturuldu." + }, + "youHaveBeenLoggedIn": { + "message": "Oturum açtınız." + }, "masterPassSent": { "message": "Size ana parolanızın ipucunu içeren bir e-posta gönderdik." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Doğrulama kodu gereklidir." }, + "webauthnCancelOrTimeout": { + "message": "Kimlik doğrulama iptal edildi ve çok uzun sürdü. Lütfen yeniden deneyin." + }, "invalidVerificationCode": { "message": "Geçersiz doğrulama kodu" }, @@ -667,17 +694,17 @@ "message": "Kimlik doğrulama uygulaması" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Kimlik doğrulama uygulamanızın (örn. Bitwarden Authenticator) ürettiği kodu girin.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP security key" + "message": "YubiKey OTP güvenlik anahtarı" }, "yubiKeyDesc": { "message": "Hesabınıza erişmek için bir YubiKey kullanın. YubiKey 4, 4 Nano, 4C ve NEO cihazlarıyla çalışır." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Duo Security'nin ürettiği kodu girin.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -694,7 +721,7 @@ "message": "E-posta" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "E-posta adresinize gönderilen kodu girin." }, "loginUnavailable": { "message": "Giriş yapılamıyor" @@ -777,6 +804,18 @@ "loginExpired": { "message": "Oturumunuzun süresi doldu." }, + "restartRegistration": { + "message": "Kaydı yeniden başlat" + }, + "expiredLink": { + "message": "Bağlantının süresi dolmuş" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Lütfen kaydı yeniden başlatın veya giriş yapmayı deneyin." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Zaten hesabınız olabilir" + }, "logOutConfirmation": { "message": "Çıkmak istediğinize emin misiniz?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Premium üyeliği bitwarden.com web kasası üzerinden satın alabilirsiniz. Şimdi siteye gitmek ister misiniz?" }, + "premiumPurchaseAlertV2": { + "message": "Bitwarden web uygulamasındaki hesap ayarlarınızdan Premium abonelik satın alabilirsiniz." + }, "premiumCurrentMember": { "message": "Premium üyesiniz!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Kopyalama başarılı" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1337,7 +1382,7 @@ "description": "ex. Date this password was updated" }, "exportFrom": { - "message": "Export from" + "message": "Dışa aktarılacak konum" }, "exportVault": { "message": "Kasayı dışa aktar" @@ -1346,31 +1391,31 @@ "message": "Dosya biçimi" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "Dışarı aktarılan bu dosya parola korumalı olacak ve dosyanın çözülmesi için parolayı girmeniz gerekecek." }, "filePassword": { - "message": "File password" + "message": "Dosya parolası" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "Bu parola, bu dosyayı dışa ve içe aktarmak için kullanılacaktır" }, "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": "Dışa aktarmayı şifrelemek ve içe aktarmayı yalnızca mevcut Bitwarden hesabıyla kısıtlamak için, hesabınızın kullanıcı adı ve ana parolasından türetilen hesap şifreleme anahtarınızı kullanın." }, "passwordProtected": { - "message": "Password protected" + "message": "Parola korumalı" }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "Dışa aktardığınız dosyayı şifrelemek ve bir Bitwarden hesabına içe aktarmak için kullanacağınız parolayı belirleyin." }, "exportTypeHeading": { - "message": "Export type" + "message": "Dışa aktarma türü" }, "accountRestricted": { - "message": "Account restricted" + "message": "Hesap kısıtlı" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "\"Dosya parolası\" ile \"Dosya parolasını onaylayın\" eşleşmiyor." }, "hCaptchaUrl": { "message": "hCaptcha adresi", @@ -1477,9 +1522,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ç" }, @@ -1492,6 +1543,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" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Yeni ana parolanız ilke gereksinimlerini karşılamıyor." }, - "receiveMarketingEmails": { - "message": "Bitwarden'dan duyurular, öneriler ve araştırmalarla ilgili e-postalar alın." + "receiveMarketingEmailsV2": { + "message": "Bitwarden'dan öneriler, duyurular ve araştırma fırsatları e-posta adresinize gelsin." }, "unsubscribe": { "message": "İstediğiniz zaman" @@ -1718,7 +1772,7 @@ "message": "Tarayıcı entegrasyonunu etkinleştirme hatası" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Tarayıcı entegrasyonu etkinleştirilirken bir hata oluştu." }, "browserIntegrationMasOnlyDesc": { "message": "Ne yazık ki tarayıcı entegrasyonu şu anda sadece Mac App Store sürümünde destekleniyor." @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "Tarayıcıda biyometriyi kullanmak için önce ayarlardan masaüstü biyometrisini ayarlamanız gerekir." }, + "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": "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." }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "E-posta doğrulaması gerekiyor" }, + "emailVerifiedV2": { + "message": "E-posta doğrulandı" + }, "emailVerificationRequiredDesc": { "message": "Bu özelliği kullanmak için e-postanızı doğrulamalısınız." }, @@ -1979,6 +2042,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" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Kasa zaman aşımınız, kuruluşunuz tarafından belirlenen kısıtlamaları aşıyor." }, + "inviteAccepted": { + "message": "Davet kabul edildi" + }, "resetPasswordPolicyAutoEnroll": { "message": "Otomatik eklenme" }, @@ -2133,7 +2202,7 @@ "message": "Hesabı değiştir" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Zaten hesabınız var mı?" }, "options": { "message": "Seçenekler" @@ -2154,10 +2223,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "Kuruluş kasasını dışa aktarma" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Yalnızca $ORGANIZATION$ ile ilişkili kuruluş kasası dışarı aktarılacak. Kişisel kasalardaki ve diğer kuruluşlardaki kayıtlar dahil edilmeyecek.", "placeholders": { "organization": { "content": "$1", @@ -2292,7 +2361,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "Geçersiz $SERVICENAME$ alan adı.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -2302,7 +2371,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "Geçersiz $SERVICENAME$ URL'si.", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -2312,7 +2381,7 @@ } }, "forwarderUnknownError": { - "message": "Unknown $SERVICENAME$ error occurred.", + "message": "Bilinmeyen $SERVICENAME$ hatası oluştu.", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -2322,7 +2391,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Unknown forwarder: '$SERVICENAME$'.", + "message": "Bilinmeyen yönlendirici: '$SERVICENAME$'.", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -2482,25 +2551,25 @@ "message": "Giriş istendi" }, "creatingAccountOn": { - "message": "Creating account on" + "message": "Hesap oluşturuluyor:" }, "checkYourEmail": { - "message": "Check your email" + "message": "E-postanızı kontrol edin" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "Hesabınızı oluşturmaya devam etmek için" }, "andContinueCreatingYourAccount": { - "message": "and continue creating your account." + "message": "adresine gönderdiğimiz e-postadaki bağlantıya tıklayın." }, "noEmail": { - "message": "No email?" + "message": "E-posta gelmedi mi?" }, "goBack": { - "message": "Go back" + "message": "Geri dönüp" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "e-posta adresinizi düzenleyin." }, "exposedMasterPassword": { "message": "Açığa Çıkmış Ana Parola" @@ -2763,14 +2832,17 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Duo'yu başlatın ve oturum açmayı tamamlamak için adımları izleyin." + "message": "Duo'yu açın ve girişi tamamlamak için adımları izleyin." }, "duoRequiredByOrgForAccount": { "message": "Hesabınız için Duo iki adımlı giriş gereklidir." }, "launchDuo": { - "message": "Duo'yu tarayıcıda başlat" + "message": "Duo'yu tarayıcıda aç" }, "importFormatError": { "message": "Veriler doğru biçimlendirilmemiş. Lütfen içe aktarma dosyanızı kontrol edin ve tekrar deneyin." @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Geçersiz dosya parolası. Lütfen dışa aktardığınız dosyayı oluştururken girdiğiniz parolayı kullanın." }, - "importDestination": { - "message": "İçe aktarma hedefi" + "destination": { + "message": "Hedef" }, "learnAboutImportOptions": { "message": "İçe aktarma seçeneklerinizi öğrenin" @@ -2797,7 +2869,7 @@ "message": "Bir koleksiyon seçin" }, "importTargetHint": { - "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", + "message": "İçe aktarılan dosya içeriklerinin $DESTINATION$ konumuna taşınmasını istiyorsanız bu seçeneği seçin", "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", "placeholders": { "destination": { @@ -2948,7 +3020,7 @@ "message": "Hedef klasör atama hatası." }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "$NAME$ içindeki kayıtları görüntüle", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -2958,7 +3030,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "$NAME$ klasörüne dön", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -2968,11 +3040,11 @@ } }, "back": { - "message": "Back", + "message": "Geri", "description": "Button text to navigate back" }, "removeItem": { - "message": "Remove $NAME$", + "message": "$NAME$ klasörünü kaldır", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Veri" + }, + "fileSends": { + "message": "Dosya Send'leri" + }, + "textSends": { + "message": "Metin Send'leri" + }, + "ssoError": { + "message": "SSO girişi için açık port bulunamadı." + }, + "fileSavedToDevice": { + "message": "Dosya cihaza kaydedildi. Cihazınızın indirilenler klasöründen yönetebilirsiniz." } } diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 9027b8d627c..2a792478744 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": "Запис додано" @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Головний пароль" + }, + "masterPassImportant": { + "message": "Головний пароль неможливо відновити, якщо ви його втратите!" + }, + "confirmMasterPassword": { + "message": "Підтвердьте головний пароль" + }, + "masterPassHintLabel": { + "message": "Підказка для головного пароля" + }, + "joinOrganization": { + "message": "Приєднатися до організації" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Завершіть приєднання до цієї організації, встановивши головний пароль." + }, "settings": { "message": "Налаштування" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Ваш обліковий запис створений! Тепер ви можете увійти." }, + "newAccountCreated2": { + "message": "Ваш обліковий запис створено!" + }, + "youHaveBeenLoggedIn": { + "message": "Ви увійшли!" + }, "masterPassSent": { "message": "Ми надіслали вам лист з підказкою для головного пароля." }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "Потрібний код підтвердження." }, + "webauthnCancelOrTimeout": { + "message": "Автентифікацію було скасовано або вона тривала надто довго. Повторіть спробу." + }, "invalidVerificationCode": { "message": "Недійсний код підтвердження" }, @@ -681,7 +708,7 @@ "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { - "message": "Авторизуйтесь за допомогою Duo Security для вашої організації з використанням мобільного додатку Duo Mobile, SMS, телефонного виклику, або ключа безпеки U2F.", + "message": "Авторизуйтесь за допомогою Duo Security для вашої організації з використанням програми Duo Mobile, SMS, телефонного виклику, або ключа безпеки U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "webAuthnTitle": { @@ -712,10 +739,10 @@ "message": "Середовище власного хостингу" }, "selfHostedEnvironmentFooter": { - "message": "Вкажіть основну URL-адресу локально розміщеного встановлення Bitwarden." + "message": "Вкажіть основну URL-адресу вашого встановлення Bitwarden на власному хостингу." }, "selfHostedBaseUrlHint": { - "message": "Вкажіть основну URL-адресу вашого локально розміщеного встановлення Bitwarden. Зразок: https://bitwarden.company.com" + "message": "Вкажіть основну URL-адресу вашого встановлення Bitwarden на власному хостингу. Зразок: https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { "message": "Для розширеної конфігурації ви можете вказати основну URL-адресу окремо для кожної служби." @@ -777,6 +804,18 @@ "loginExpired": { "message": "Тривалість вашого сеансу завершилась." }, + "restartRegistration": { + "message": "Перезапустити реєстрацію" + }, + "expiredLink": { + "message": "Протерміноване посилання" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Перезапустіть реєстрацію або спробуйте ввійти." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Можливо, ви вже зареєстровані" + }, "logOutConfirmation": { "message": "Ви дійсно хочете вийти?" }, @@ -849,7 +888,7 @@ "message": "Перейти у вебсховище" }, "getMobileApp": { - "message": "Отримати мобільний додаток" + "message": "Отримати програму для мобільних пристроїв" }, "getBrowserExtension": { "message": "Отримати розширення браузера" @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Ви можете передплатити преміум у сховищі на bitwarden.com. Хочете перейти на вебсайт зараз?" }, + "premiumPurchaseAlertV2": { + "message": "Ви можете придбати Преміум у налаштуваннях облікового запису вебпрограмі Bitwarden." + }, "premiumCurrentMember": { "message": "Ви користуєтеся передплатою преміум!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Успішно скопійовано" + }, "errorRefreshingAccessToken": { "message": "Помилка оновлення токена доступу" }, @@ -1477,9 +1522,15 @@ "additionalWindowsHelloSettings": { "message": "Додаткові налаштування Windows Hello" }, + "unlockWithPolkit": { + "message": "Розблокувати за допомогою системної автентифікації" + }, "windowsHelloConsentMessage": { "message": "Перевірити на Bitwarden." }, + "polkitConsentMessage": { + "message": "Автентифікуйтесь для розблокування Bitwarden." + }, "unlockWithTouchId": { "message": "Розблокувати з Touch ID" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "Запитувати Windows Hello під час запуску" }, + "autoPromptPolkit": { + "message": "Запитувати системну автентифікацію під час запуску" + }, "autoPromptTouchId": { "message": "Запитувати Touch ID під час запуску" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "Ваш новий головний пароль не задовольняє вимоги політики." }, - "receiveMarketingEmails": { - "message": "Отримуйте електронні листи від Bitwarden з оголошеннями, порадами та інформацією про нові можливості." + "receiveMarketingEmailsV2": { + "message": "Отримуйте поради, оголошення та можливості дослідження від Bitwarden у своїй поштовій скриньці." }, "unsubscribe": { "message": "Відписатися" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "Для використання біометрії в браузері необхідно спершу налаштувати її в програмі на комп'ютері." }, + "biometricsManualSetupTitle": { + "message": "Автоналаштування недоступне" + }, + "biometricsManualSetupDesc": { + "message": "У зв'язку з методом встановлення, не можна автоматично ввімкнути підтримку біометрії. Бажаєте переглянути документацію про те, як зробити це вручну?" + }, "personalOwnershipSubmitError": { "message": "Згідно з політикою компанії, вам заборонено зберігати записи в особистому сховищі. Змініть опцію власника на організацію та виберіть серед доступних збірок." }, @@ -1781,7 +1841,7 @@ "message": "Політика організації впливає на ваші параметри власності." }, "personalOwnershipPolicyInEffectImports": { - "message": "Політика організації заблокувала імпортування елементів до вашого особистого сховища." + "message": "Політика організації заблокувала імпортування записів до вашого особистого сховища." }, "allSends": { "message": "Усі відправлення", @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "Необхідно підтвердити е-пошту" }, + "emailVerifiedV2": { + "message": "Електронну пошту підтверджено" + }, "emailVerificationRequiredDesc": { "message": "Для використання цієї функції необхідно підтвердити електронну пошту." }, @@ -1979,6 +2042,9 @@ "updateWeakMasterPasswordWarning": { "message": "Ваш головний пароль не відповідає одній або більше політикам вашої організації. Щоб отримати доступ до сховища, вам необхідно оновити свій головний пароль зараз. Продовживши, ви вийдете з поточного сеансу, після чого потрібно буде повторно виконати вхід. Сеанси на інших пристроях можуть залишатися активними протягом однієї години." }, + "tdeDisabledMasterPasswordRequired": { + "message": "Ваша організація вимкнула шифрування довірених пристроїв. Встановіть головний пароль для доступу до сховища." + }, "tryAgain": { "message": "Спробуйте знову" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "Час очікування сховища перевищує обмеження, встановлені вашою організацією." }, + "inviteAccepted": { + "message": "Запрошення прийнято" + }, "resetPasswordPolicyAutoEnroll": { "message": "Автоматичне подання запиту" }, @@ -2157,7 +2226,7 @@ "message": "Експортування сховища організації" }, "exportingOrganizationVaultDesc": { - "message": "Буде експортовано лише сховище організації, пов'язане з $ORGANIZATION$. Елементи особистих сховищ або інших організацій не будуть включені.", + "message": "Буде експортовано лише сховище організації, пов'язане з $ORGANIZATION$. Записи особистих сховищ або інших організацій не будуть включені.", "placeholders": { "organization": { "content": "$1", @@ -2743,7 +2812,7 @@ "message": "Дані успішно імпортовано" }, "importSuccessNumberOfItems": { - "message": "Всього імпортовано $AMOUNT$ елементів.", + "message": "Всього імпортовано $AMOUNT$ записів.", "placeholders": { "amount": { "content": "$1", @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Помилка під'єднання до служби Duo. Скористайтеся іншим способом двоетапної перевірки або зверніться до служби підтримки Duo по допомогу." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Запустіть Duo і виконайте дії для завершення входу." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "Неправильний пароль файлу. Використайте пароль, який ви вводили під час створення експортованого файлу." }, - "importDestination": { - "message": "Призначення імпорту" + "destination": { + "message": "Призначення" }, "learnAboutImportOptions": { "message": "Дізнайтеся про параметри імпорту" @@ -2807,7 +2879,7 @@ } }, "importUnassignedItemsError": { - "message": "Файл містить непризначені елементи." + "message": "Файл містить непризначені записи." }, "selectFormat": { "message": "Оберіть формат імпортованого файлу" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Дані" + }, + "fileSends": { + "message": "Відправлення файлів" + }, + "textSends": { + "message": "Відправлення тексту" + }, + "ssoError": { + "message": "Не знайдено вільних портів для цього входу SSO." + }, + "fileSavedToDevice": { + "message": "Файл збережено на пристрої. Ви можете його знайти у теці завантажень." } } diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index 6e155b82a75..26cc73ba81a 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -12,7 +12,7 @@ "message": "Yêu thích" }, "types": { - "message": "Các loại" + "message": "Loại" }, "typeLogin": { "message": "Đăng nhập" @@ -36,7 +36,7 @@ "message": "Tìm kiếm trong Kho" }, "addItem": { - "message": "Thêm Mục" + "message": "Thêm" }, "shared": { "message": "Đã chia sẻ" @@ -61,19 +61,19 @@ } }, "moveToOrgDesc": { - "message": "Chọn một tổ chức mà bạn muốn chuyển mục này tới. Việc di chuyển đến một tổ chức sẽ chuyển quyền sở hữu của mục sang tổ chức mà bạn chọn. Bạn sẽ không còn là chủ sở hữu trực tiếp của mục này một khi nó đã được chuyển." + "message": "Chọn một tổ chức mà bạn muốn chuyển mục này đến. Việc chuyển đến một tổ chức sẽ chuyển quyền sở hữu mục này cho tổ chức đó. Bạn sẽ không còn là chủ sở hữu trực tiếp của mục này khi đã chuyển." }, "attachments": { - "message": "Tệp đính kèm" + "message": "Đính kèm" }, "viewItem": { - "message": "Xem mục" + "message": "Chi tiết" }, "name": { - "message": "Tên mục" + "message": "Tên" }, "uri": { - "message": "URL" + "message": "Đường dẫn" }, "uriPosition": { "message": "URL $POSITION$", @@ -86,7 +86,7 @@ } }, "newUri": { - "message": "URL Mới" + "message": "URI Mới" }, "username": { "message": "Tên người dùng" @@ -98,16 +98,16 @@ "message": "Cụm từ mật khẩu" }, "editItem": { - "message": "Chỉnh sửa mục" + "message": "Chỉnh sửa" }, "emailAddress": { - "message": "Địa chỉ Email" + "message": "Địa chỉ email" }, "verificationCodeTotp": { "message": "Mã xác thực (TOTP)" }, "website": { - "message": "Trang web" + "message": "Địa chỉ" }, "notes": { "message": "Ghi chú" @@ -123,16 +123,16 @@ "description": "Copy value to clipboard" }, "minimizeOnCopyToClipboard": { - "message": "Thu nhỏ sau khi sao chép vào bộ nhớ đệm" + "message": "Thu nhỏ sau khi sao chép vào khay nhớ tạm" }, "minimizeOnCopyToClipboardDesc": { - "message": "Thu nhỏ sau khi sao chép thông tin của một mục vào bộ nhớ đệm." + "message": "Thu nhỏ ứng dụng sau khi sao chép thông tin của một mục vào khay nhớ tạm." }, "toggleVisibility": { "message": "Bật/tắt khả năng hiển thị" }, "toggleCollapse": { - "message": "Toggle Collapse", + "message": "Bật/tắt thu gọn", "description": "Toggling an expand/collapse state." }, "cardholderName": { @@ -151,7 +151,7 @@ "message": "Mã bảo mật" }, "identityName": { - "message": "Tên đầy đủ" + "message": "Tên danh tính" }, "company": { "message": "Công ty" @@ -181,7 +181,7 @@ "message": "Cần là thành viên cao cấp để sử dụng tính năng này." }, "errorOccurred": { - "message": "Đã xảy ra lỗi chưa xác định." + "message": "Đã xảy ra lỗi." }, "error": { "message": "Lỗi" @@ -245,7 +245,7 @@ "message": "Tiến sĩ" }, "expirationMonth": { - "message": "Tháng Hết Hạn" + "message": "Tháng hết hạn" }, "expirationYear": { "message": "Năm hết hạn" @@ -352,7 +352,7 @@ "message": "Đã thêm mục" }, "editedItem": { - "message": "Mục được chỉnh sửa" + "message": "Đã lưu mục" }, "deleteItem": { "message": "Xóa mục" @@ -361,13 +361,13 @@ "message": "Xóa thư mục" }, "deleteAttachment": { - "message": "Gỡ bỏ tập tin đính kèm" + "message": "Xoá tệp đính kèm" }, "deleteItemConfirmation": { - "message": "Bạn có chắc bạn muốn xóa mục này?" + "message": "Bạn có chắc muốn cho vào thùng rác?" }, "deletedItem": { - "message": "Đã xóa mục" + "message": "Mục đã được cho vào thùng rác" }, "overwritePasswordConfirmation": { "message": "Bạn có chắc chắn muốn ghi đè mật khẩu hiện tại không?" @@ -376,10 +376,10 @@ "message": "Ghi đè tên người dùng" }, "overwriteUsernameConfirmation": { - "message": "Bạn có chắc là muốn ghi đè tên người dùng hiện tại không?" + "message": "Bạn có chắc muốn ghi đè lên tên người dùng hiện tại không?" }, "noneFolder": { - "message": "Không được phân loại", + "message": "Chưa phân loại", "description": "This is the folder for uncategorized items" }, "addFolder": { @@ -404,7 +404,7 @@ "message": "Độ dài" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "Độ dài mật khẩu tối thiểu" }, "uppercase": { "message": "Chữ in hoa (A-Z)" @@ -419,10 +419,10 @@ "message": "Ký tự đặc biệt (!@#$%^&*)" }, "numWords": { - "message": "Number of Words" + "message": "Số lượng chữ" }, "wordSeparator": { - "message": "Word Separator" + "message": "Dấu tách từ" }, "capitalize": { "message": "Viết hoa", @@ -435,14 +435,14 @@ "message": "Đóng" }, "minNumbers": { - "message": "Số kí tự tối thiểu" + "message": "Số chữ số" }, "minSpecial": { - "message": "Số kí tự đặc biệt tối thiểu", + "message": "Số kí tự đặc biệt", "description": "Minimum Special Characters" }, "ambiguous": { - "message": "Tránh các ký tự không rõ ràng" + "message": "Tránh các ký tự dễ gây nhầm lẫn" }, "searchCollection": { "message": "Tìm kiếm bộ sưu tập" @@ -476,13 +476,13 @@ "message": "Chọn 1 tập tin." }, "maxFileSize": { - "message": "Kích thước tối đa của tệp tin là 500MB." + "message": "Kích thước tối đa của tập tin là 500MB." }, "encryptionKeyMigrationRequired": { - "message": "Encryption key migration required. Please login through the web vault to update your encryption key." + "message": "Cần di chuyển khóa mã hóa. Vui lòng đăng nhập trang web Bitwaden để cập nhật khóa mã hóa của bạn." }, "editedFolder": { - "message": "Đã chỉnh sửa thư mục" + "message": "Đã lưu thư mục" }, "addedFolder": { "message": "Đã thêm thư mục" @@ -497,13 +497,13 @@ "message": "Đăng nhập hoặc tạo tài khoản mới để truy cập kho mật khẩu của bạn." }, "createAccount": { - "message": "Tạo Tài Khoản" + "message": "Tạo tài khoản" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Đặt mật khẩu mạnh" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" + "message": "Hoàn thành việc tạo tài khoản của bạn bằng cách đặt mật khẩu" }, "logIn": { "message": "Đăng Nhập" @@ -515,7 +515,7 @@ "message": "Mật khẩu chính" }, "masterPassDesc": { - "message": "Mật khẩu chính là mật khẩu bạn sử dụng để truy cập kho mật khẩu của bạn. Nó rất quan trọng nên bạn không được quên mật khẩu chính của mình. Không có cách nào để khôi phục lại mật khẩu chính nếu bạn quên nó." + "message": "Mật khẩu chính là mật khẩu bạn sử dụng để truy cập kho mật khẩu của bạn. Nó rất quan trọng vì sẽ không có cách nào để lấy lại mật khẩu nếu bạn quên." }, "masterPassHintDesc": { "message": "Một gợi ý mật khẩu có thể giúp bạn nhớ lại mật khẩu chính của bạn nếu bạn quên nó." @@ -527,7 +527,7 @@ "message": "Gợi ý mật khẩu chính (tùy chọn)" }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Nếu bạn quên mật khẩu, gợi ý mật khẩu có thể được gửi tới email của bạn. $CURRENT$/$MAXIMUM$ ký tự tối đa.", "placeholders": { "current": { "content": "$1", @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Mật khẩu chính" + }, + "masterPassImportant": { + "message": "Mật khẩu chính của bạn không thể phục hồi nếu bạn quên nó!" + }, + "confirmMasterPassword": { + "message": "Nhập lại mật khẩu chính" + }, + "masterPassHintLabel": { + "message": "Gợi ý mật khẩu chính" + }, + "joinOrganization": { + "message": "Tham gia tổ chức" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Hoàn tất gia nhập tổ chức này bằng cách đặt một mật khẩu chính." + }, "settings": { "message": "Cài đặt" }, @@ -574,10 +592,10 @@ } }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Bạn đã đăng nhập thành công" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Bạn có thể đóng cửa sổ này" }, "masterPassDoesntMatch": { "message": "Xác nhận mật khẩu chính không khớp." @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "Tài khoản của bạn đã được tạo. Bạn có thể đăng nhập bây giờ." }, + "newAccountCreated2": { + "message": "Tài khoản của bạn đã được tạo thành công!" + }, + "youHaveBeenLoggedIn": { + "message": "Bạn đã đăng nhập thành công!" + }, "masterPassSent": { "message": "Chúng tôi đã gửi cho bạn email có chứa gợi ý mật khẩu chính của bạn." }, @@ -592,10 +616,10 @@ "message": "Một lỗi bất ngờ đã xảy ra." }, "itemInformation": { - "message": "Mục thông tin" + "message": "Thông tin mục" }, "noItemsInList": { - "message": "Không có mục nào để liệt kê." + "message": "Không có mục nào." }, "sendVerificationCode": { "message": "Gửi mã xác minh đến email của bạn" @@ -607,22 +631,25 @@ "message": "Đã gửi mã" }, "verificationCode": { - "message": "Mã xác nhận" + "message": "Mã xác minh" }, "confirmIdentity": { - "message": "Xác minh danh tính của bạn để tiếp tục." + "message": "Xác minh danh tính để tiếp tục." }, "verificationCodeRequired": { - "message": "Yêu cầu mã xác nhận." + "message": "Yêu cầu mã xác minh." + }, + "webauthnCancelOrTimeout": { + "message": "Quá trình xác thực đã bị hủy hoặc mất quá nhiều thời gian. Vui lòng thử lại." }, "invalidVerificationCode": { - "message": "Mã xác minh không hợp lệ" + "message": "Mã xác minh không đúng" }, "continue": { "message": "Tiếp tục" }, "enterVerificationCodeApp": { - "message": "Nhập mã xác nhận 6 chữ số từ ứng dụng xác thực của bạn." + "message": "Nhập mã xác minh 6 chữ số từ ứng dụng xác thực của bạn." }, "enterVerificationCodeEmail": { "message": "Nhập mã xác nhận 6 chữ số đã được gửi tới $EMAIL$.", @@ -646,88 +673,88 @@ "message": "Ghi nhớ đăng nhập" }, "sendVerificationCodeEmailAgain": { - "message": "Gửi lại email chứa mã xác nhận" + "message": "Gửi lại email chứa mã xác minh" }, "useAnotherTwoStepMethod": { - "message": "Sử dụng phương pháp xác thực hai lớp khác" + "message": "Sử dụng phương pháp xác minh hai lớp khác" }, "insertYubiKey": { - "message": "Lắp YubiKey vào cổng USB máy tính của bạn, sau đó chạm vào nút trên nó." + "message": "Cắm YubiKey vào cổng USB trên máy tính bạn và bấm nút trên Yubikey." }, "insertU2f": { "message": "Lắp khóa bảo mật vào cổng USB của máy tính. Nếu nó có một nút, nhấn vào nó." }, "recoveryCodeDesc": { - "message": "Bạn mất quyền truy cập vào tất cả các dịch vụ xác thực 2 lớp? Sử dụng mã phục hồi của bạn để vô hiệu hóa tất cả các dịch vụ xác thực hai lớp trong tài khoản của bạn." + "message": "Bạn mất quyền truy cập vào tất cả các dịch vụ xác thực hai bước? Sử dụng mã phục hồi của bạn để vô hiệu hóa tất cả các dịch vụ xác thực hai bước trong tài khoản của bạn." }, "recoveryCodeTitle": { "message": "Mã phục hồi" }, "authenticatorAppTitle": { - "message": "Ứng dụng xác thực" + "message": "Ứng dụng Authenticator" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "Nhập mã được tạo bởi ứng dụng xác thực như Bitwarden Authenticator.", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP security key" + "message": "Khóa bảo mật OTP Yubico" }, "yubiKeyDesc": { - "message": "Sử dụng YubiKey để truy cập tài khoản của bạn. Làm việc với thiết bị YubiKey 4, 4 Nano, 4C và NEO." + "message": "Sử dụng YubiKey để truy cập tài khoản của bạn. Hoạt động với thiết bị YubiKey 4, 4 Nano, 4C và NEO." }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "Nhập mã được tạo bởi Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { - "message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.", + "message": "Xác minh với Duo Security cho tổ chức của bạn sử dụng ứng dụng Duo Mobile, SMS, cuộc gọi điện thoại, hoặc khoá bảo mật U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "webAuthnTitle": { "message": "FIDO2 WebAuthn" }, "webAuthnDesc": { - "message": "Sử dụng bất kỳ khoá bảo mật được kích hoạt WebAuthn nào để truy cập tài khoản của bạn." + "message": "Sử dụng bất kỳ khóa bảo mật nào tương thích với WebAuthn để truy cập tài khoản của bạn." }, "emailTitle": { "message": "Email" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Nhập mã được gửi về email của bạn." }, "loginUnavailable": { - "message": "Đăng nhập không sẵn có" + "message": "Đăng nhập không được" }, "noTwoStepProviders": { - "message": "Tài khoản này đã kích hoạt xác thực hai lớp, tuy nhiên, thiết bị này không hỗ trợ cấu hình dịch vụ xác thực hai lớp đang sử dụng." + "message": "Tài khoản này đã thiết lập xác minh hai bước. Tuy nhiên, thiết bị này không hỗ trợ dịch vụ xác minh hai bước đang sử dụng." }, "noTwoStepProviders2": { "message": "Vui lòng thêm các nhà cung cấp khác được hỗ trợ tốt hơn trên các thiết bị (chẳng hạn như một ứng dụng xác thực)." }, "twoStepOptions": { - "message": "Tùy chọn xác thực hai lớp" + "message": "Tùy chọn xác minh hai bước" }, "selfHostedEnvironment": { - "message": "Môi trường độc lập" + "message": "Môi trường tự lưu trữ" }, "selfHostedEnvironmentFooter": { - "message": "Chỉ định liên kết cơ bản của cài đặt Bitwarden tại chỗ của bạn." + "message": "Chỉ định địa chỉ cơ sở của bản cài đặt Bitwarden được lưu trữ tại máy chủ của bạn." }, "selfHostedBaseUrlHint": { - "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" + "message": "Nhập địa chỉ cơ sở của bản cài đặt Bitwarden được lưu trữ tại máy chủ của bạn. Ví dụ: https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { - "message": "For advanced configuration, you can specify the base URL of each service independently." + "message": "Đối với cấu hình nâng cao. Bạn có thể chỉ định địa chỉ cơ sở của mỗi dịch vụ một cách độc lập." }, "selfHostedEnvFormInvalid": { - "message": "You must add either the base Server URL or at least one custom environment." + "message": "Bạn phải thêm địa chỉ máy chủ cơ sở hoặc ít nhất một môi trường tùy chỉnh." }, "customEnvironment": { "message": "Môi trường tùy chỉnh" }, "customEnvironmentFooter": { - "message": "Đối với người dùng nâng cao. Bạn có thể chỉ định liên kết cơ bản của mỗi dịch vụ một cách độc lập." + "message": "Đối với người dùng nâng cao. Bạn có thể chỉ định địa chỉ cơ sở của mỗi dịch vụ một cách độc lập." }, "baseUrl": { "message": "Địa chỉ máy chủ" @@ -736,16 +763,16 @@ "message": "Địa chỉ API máy chủ" }, "webVaultUrl": { - "message": "Địa chỉ máy chủ kho web" + "message": "Địa chỉ máy chủ lưu trữ web" }, "identityUrl": { "message": "Địa chỉ nhận dạng máy chủ" }, "notificationsUrl": { - "message": "Notifications Server URL" + "message": "Địa chỉ máy chủ thông báo" }, "iconsUrl": { - "message": "Biểu tượng địa chỉ máy chủ" + "message": "Địa chỉ biểu tượng máy chủ" }, "environmentSaved": { "message": "Địa chỉ môi trường đã được lưu." @@ -772,11 +799,23 @@ "message": "Đăng xuất" }, "loggedOutDesc": { - "message": "You have been logged out of your account." + "message": "Bạn đã đăng xuất khỏi tài khoản của mình." }, "loginExpired": { "message": "Phiên đăng nhập của bạn đã hết hạn." }, + "restartRegistration": { + "message": "Tiến hành đăng ký lại" + }, + "expiredLink": { + "message": "Liên kết đã hết hạn" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Vui lòng đăng ký lại hoặc thử đăng nhập." + }, + "youMayAlreadyHaveAnAccount": { + "message": "Bạn có thể đã có tài khoản" + }, "logOutConfirmation": { "message": "Bạn có chắc chắn muốn đăng xuất không?" }, @@ -784,13 +823,13 @@ "message": "Đăng xuất" }, "addNewLogin": { - "message": "Thêm đăng nhập mới" + "message": "Đăng nhập mới" }, "addNewItem": { - "message": "Thêm mục mới" + "message": "Mục mới" }, "addNewFolder": { - "message": "Thêm thư mục mới" + "message": "Thư mục mới" }, "view": { "message": "Xem" @@ -805,7 +844,7 @@ "message": "Khóa kho" }, "passwordGenerator": { - "message": "Tạo mật khẩu" + "message": "Trình tạo mật khẩu" }, "contactUs": { "message": "Liên hệ với chúng tôi" @@ -823,30 +862,30 @@ "message": "Blog" }, "followUs": { - "message": "Theo chúng tôi" + "message": "Theo dõi chúng tôi" }, "syncVault": { - "message": "Đồng bộ Kho mật khẩu" + "message": "Đồng bộ kho" }, "changeMasterPass": { "message": "Thay đổi mật khẩu chính" }, "continueToWebApp": { - "message": "Continue to web app?" + "message": "Tiếp tục tới ứng dụng web?" }, "changeMasterPasswordOnWebConfirmation": { - "message": "You can change your master password on the Bitwarden web app." + "message": "Bạn có thể thay đổi mật khẩu chính của mình trên Bitwarden bản web." }, "fingerprintPhrase": { - "message": "Fingerprint Phrase", + "message": "Cụm vân tay", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." }, "yourAccountsFingerprint": { - "message": "Cụm từ mật khẩu tài khoản của bạn", + "message": "Cụm vân tay tài khoản của bạn", "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": "Đi đến bitwarden nền Web" + "message": "Đi đến Bitwarden bản web" }, "getMobileApp": { "message": "Tải ứng dụng điện thoại" @@ -861,7 +900,7 @@ "message": "Đồng bộ thất bại" }, "yourVaultIsLocked": { - "message": "Kho mật khẩu đã bị khóa. Xác minh mật khẩu chính của bạn để mở." + "message": "Kho của bạn đã bị khóa. Xác minh danh tính của bạn để mở khoá." }, "unlock": { "message": "Mở khóa" @@ -883,13 +922,13 @@ "message": "Mật khẩu chính không hợp lệ" }, "twoStepLoginConfirmation": { - "message": "Xác thực hai lớp giúp cho tài khoản của bạn an toàn hơn bằng cách yêu cầu bạn xác minh thông tin đăng nhập của bạn bằng một thiết bị khác như khóa bảo mật, ứng dụng xác thực, SMS, cuộc gọi điện thoại hoặc email. Bạn có thể bật xác thực hai lớp trong kho bitwarden nền web. Bạn có muốn ghé thăm trang web bây giờ?" + "message": "Xác minh 2 bước giúp tài khoản của bạn an toàn hơn bằng cách yêu cầu bạn xác minh bằng một thiết bị khác như khóa bảo mật, ứng dụng xác thực, SMS, cuộc gọi điện thoại hoặc email. Xác minh 2 bước có thể được thiết lập trên bitwarden.com. Bạn có muốn truy cập trang web bây giờ không?" }, "twoStepLogin": { - "message": "Xác thực hai lớp" + "message": "Xác minh hai bước" }, "vaultTimeout": { - "message": "Thời Gian Chờ Của Kho" + "message": "Thời gian mở kho" }, "vaultTimeoutDesc": { "message": "Chọn khi nào thì kho của bạn sẽ hết thời gian chờ và thực hiện hành động đã được chọn." @@ -946,45 +985,45 @@ "message": "Bảo mật" }, "clearClipboard": { - "message": "Dọn dẹp khay nhớ tạm", + "message": "Xóa khay nhớ tạm", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "clearClipboardDesc": { - "message": "Tự động dọn dẹp giá trị được sao chép khỏi khay nhớ tạm của bạn.", + "message": "Tự động xóa mọi thứ đã sao chép khỏi khay nhớ tạm.", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "enableFavicon": { "message": "Hiện biểu tượng trang web" }, "faviconDesc": { - "message": "Hiện một ảnh nhận dạng bên cạnh mỗi lần đăng nhập." + "message": "Hiện logo trang web bên cạnh mỗi đăng nhập." }, "enableMinToTray": { - "message": "Minimize to Tray Icon" + "message": "Thu nhỏ vào khay hệ thống" }, "enableMinToTrayDesc": { - "message": "When minimizing the window, show an icon in the system tray instead." + "message": "Khi thu nhỏ cửa sổ, thay vào đó sẽ hiện một biểu tượng trên khay hệ thống." }, "enableMinToMenuBar": { - "message": "Thu nhỏ về thanh menu" + "message": "Thu nhỏ vào thanh menu" }, "enableMinToMenuBarDesc": { - "message": "When minimizing the window, show an icon in the menu bar instead." + "message": "Khi thu nhỏ sổ, thay vào đó sẽ hiện một biểu tượng trên thanh menu." }, "enableCloseToTray": { - "message": "Close to Tray Icon" + "message": "Đóng vào khay hệ thống" }, "enableCloseToTrayDesc": { "message": "Khi đóng cửa sổ, thay vào đó sẽ hiện một biểu tượng trên khay hệ thống." }, "enableCloseToMenuBar": { - "message": "Close to menu bar" + "message": "Đóng vào thanh menu" }, "enableCloseToMenuBarDesc": { - "message": "When closing the window, show an icon in the menu bar instead." + "message": "Khi đóng cửa sổ, thay vào đó sẽ hiện một biểu tượng trên thanh menu." }, "enableTray": { - "message": "Enable Tray Icon" + "message": "Hiện biểu tượng khay hệ thống" }, "enableTrayDesc": { "message": "Luôn hiện biểu tượng trên khay hệ thống." @@ -996,22 +1035,22 @@ "message": "Khi ứng dụng mới mở, chỉ hiện biểu tượng trên khay hệ thống." }, "startToMenuBar": { - "message": "Start to menu bar" + "message": "Khởi động vào thanh menu" }, "startToMenuBarDesc": { - "message": "When the application is first started, only show an icon in the menu bar." + "message": "Khi ứng dụng mới mở, chỉ hiện biểu tượng trên thanh menu." }, "openAtLogin": { - "message": "Tự động bắt đầu khi đăng nhập" + "message": "Khởi động cùng lúc với máy tính" }, "openAtLoginDesc": { - "message": "Tự động chạy ứng dụng máy tính Bitwarden khi đăng nhập." + "message": "Tự động khởi động ứng dụng Bitwarden trên máy tính khi đăng nhập." }, "alwaysShowDock": { "message": "Luôn hiện ở thanh Dock" }, "alwaysShowDockDesc": { - "message": "Hiện biểu tượng Bitwarden trong Dock ngày cả khi thu nhỏ về thanh hệ thống." + "message": "Hiện biểu tượng Bitwarden trong Dock ngay cả khi thu nhỏ về thanh menu." }, "confirmTrayTitle": { "message": "Xác nhận ẩn khay" @@ -1089,7 +1128,7 @@ "message": "Không xác định" }, "copyUsername": { - "message": "Sao chép Tên đăng nhập" + "message": "Sao chép tên đăng nhập" }, "copyNumber": { "message": "Chép số", @@ -1118,16 +1157,16 @@ "message": "Đăng ký làm thành viên cao cấp và nhận được:" }, "premiumSignUpStorage": { - "message": "1GB bộ nhớ lưu trữ tập tin được mã hóa." + "message": "1GB bộ nhớ lưu trữ được mã hóa cho các tệp đính kèm." }, "premiumSignUpTwoStepOptions": { - "message": "Proprietary two-step login options such as YubiKey and Duo." + "message": "Các tùy chọn xác minh hai bước như YubiKey và Duo." }, "premiumSignUpReports": { "message": "Thanh lọc mật khẩu, kiểm tra an toàn tài khoản và các báo cáo rò rĩ dữ liệu là để giữ cho kho của bạn an toàn." }, "premiumSignUpTotp": { - "message": "Mã xác nhận TOTP (2FA) để đăng nhập vào kho mật khẩu của bạn." + "message": "Mã xác nhận TOTP (2FA) để đăng nhập vào kho của bạn." }, "premiumSignUpSupport": { "message": "Hỗ trợ khách hàng ưu tiên." @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "Bạn có thể nâng cấp làm thành viên cao cấp trong kho bitwarden nền web. Bạn có muốn truy cập trang web bây giờ?" }, + "premiumPurchaseAlertV2": { + "message": "Bạn có thể mua gói Premium từ cài đặt tài khoản trên trang Bitwarden." + }, "premiumCurrentMember": { "message": "Bạn là một thành viên cao cấp!" }, @@ -1160,14 +1202,14 @@ "message": "Làm mới hoàn tất" }, "passwordHistory": { - "message": "Lịch sử Mật khẩu" + "message": "Lịch sử mật khẩu" }, "clear": { "message": "Xoá", "description": "To clear something out. example: To clear browser history." }, "noPasswordsInList": { - "message": "Không có mật khẩu để liệt kê." + "message": "Chưa có mật khẩu." }, "undo": { "message": "Hoàn tác" @@ -1243,11 +1285,14 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { - "message": "Access Token Refresh Error" + "message": "Lỗi làm mới khoá truy cập" }, "errorRefreshingAccessTokenDesc": { - "message": "No refresh token or API keys found. Please try logging out and logging back in." + "message": "Bạn có thể đã bị đăng xuất. Vui lòng đăng xuất và đăng nhập lại." }, "help": { "message": "Trợ giúp" @@ -1259,7 +1304,7 @@ "message": "Kiểm tra xem mật khẩu có bị lộ không." }, "passwordExposed": { - "message": "Mật khẩu này đã bị lộ $VALUE$ thời gian trong các dữ liệu vi phạm. Bạn nên thay đổi nó.", + "message": "Mật khẩu này đã bị lộ $VALUE$ lần trong các vụ rò rỉ dữ liệu. Bạn nên đổi nó.", "placeholders": { "value": { "content": "$1", @@ -1268,7 +1313,7 @@ } }, "passwordSafe": { - "message": "Mật khẩu này không được tìm thấy trong bất kỳ dữ liệu vi phạm nào được biết đến. Nó an toàn để sử dụng." + "message": "Mật khẩu này không tìm thấy trong bất kỳ vụ rò rỉ dữ liệu nào. Nó an toàn để sử dụng." }, "baseDomain": { "message": "Tên miền cơ sở", @@ -1304,7 +1349,7 @@ "message": "Bật/tắt tùy chọn" }, "organization": { - "message": "Organization", + "message": "Tổ chức", "description": "An entity of multiple related people (ex. a team or business organization)." }, "default": { @@ -1314,18 +1359,18 @@ "message": "Thoát ra" }, "showHide": { - "message": "Show / Hide", + "message": "Hiển thị / Ẩn", "description": "Text for a button that toggles the visibility of the window. Shows the window when it is hidden or hides the window if it is currently open." }, "hideToTray": { - "message": "Hide to Tray" + "message": "Ẩn xuống khay hệ thống" }, "alwaysOnTop": { "message": "Luôn trên cùng", "description": "Application window should always stay on top of other windows" }, "dateUpdated": { - "message": "Ngày cập nhật", + "message": "Cập nhật vào", "description": "ex. Date this item was updated" }, "dateCreated": { @@ -1333,103 +1378,103 @@ "description": "ex. Date this item was created" }, "datePasswordUpdated": { - "message": "Password Updated", + "message": "Đã cập nhật mật khẩu", "description": "ex. Date this password was updated" }, "exportFrom": { - "message": "Export from" + "message": "Xuất từ" }, "exportVault": { - "message": "Export Vault" + "message": "Xuất kho" }, "fileFormat": { - "message": "File Format" + "message": "Định dạng tập tin" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "Tập tin xuất này sẽ được bảo vệ bằng mật khẩu và yêu cầu mật khẩu để giải mã." }, "filePassword": { - "message": "File password" + "message": "Mật khẩu tập tin" }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "Mật khẩu này sẽ được sử dụng để xuất và nhập tập tin này" }, "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": "Sử dụng khóa mã hóa tài khoản của bạn, được tạo từ tên người dùng và mật khẩu chính của bạn để mã hóa tệp xuất và giới hạn việc nhập chỉ cho tài khoản Bitwarden hiện tại." }, "passwordProtected": { - "message": "Password protected" + "message": "Mật khẩu đã được bảo vệ" }, "passwordProtectedOptionDescription": { - "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "Thiết lập mật khẩu cho tệp để mã hóa dữ liệu xuất và nhập nó vào bất kỳ tài khoản Bitwarden nào bằng cách sử dụng mật khẩu đó để giải mã." }, "exportTypeHeading": { - "message": "Export type" + "message": "Loại xuất" }, "accountRestricted": { - "message": "Account restricted" + "message": "Tài khoản bị hạn chế" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "“Mật khẩu tập tin” và “Nhập lại mật khẩu tập tin” không khớp." }, "hCaptchaUrl": { - "message": "Url hCaptcha", + "message": "Địa chỉ hCaptcha", "description": "hCaptcha is the name of a website, should not be translated" }, "loadAccessibilityCookie": { - "message": "Tải cookie hỗ trợ tiếp cận" + "message": "Tải cookie trợ năng" }, "registerAccessibilityUser": { - "message": "Register as an accessibility user at", + "message": "Đăng ký như người dùng trợ năng tại", "description": "ex. Register as an accessibility user at hcaptcha.com" }, "copyPasteLink": { - "message": "Copy and paste the link sent to your email below" + "message": "Sao chép và dán liên kết được gửi tới email của bạn bên dưới" }, "enterhCaptchaUrl": { - "message": "Enter URL to load accessibility cookie for hCaptcha", + "message": "Nhập địa chỉ để tải cookie trợ năng cho hCaptcha", "description": "hCaptcha is the name of a website, should not be translated" }, "hCaptchaUrlRequired": { - "message": "hCaptcha Url is required", + "message": "Địa chỉ hCaptcha là bắt buộc", "description": "hCaptcha is the name of a website, should not be translated" }, "invalidUrl": { - "message": "Url không hợp lệ" + "message": "Địa chỉ không hợp lệ" }, "done": { "message": "Xong" }, "accessibilityCookieSaved": { - "message": "Accessibility cookie saved!" + "message": "Đã lưu cookie trợ năng!" }, "noAccessibilityCookieSaved": { - "message": "Không có cookie hỗ trợ tương tác nào được lưu" + "message": "Không có cookie trợ năng nào được lưu" }, "warning": { "message": "CẢNH BÁO", "description": "WARNING (should stay in capitalized letters if the language permits)" }, "confirmVaultExport": { - "message": "Xác nhận xuất kho lưu trữ" + "message": "Xác nhận xuất kho" }, "exportWarningDesc": { - "message": "This export contains your vault data in an unencrypted format. You should not store or send the exported file over unsecure channels (such as email). Delete it immediately after you are done using it." + "message": "Bản xuất này chứa dữ liệu kho bạn và không được mã hóa. Bạn không nên lưu trữ hay gửi tập tin đã xuất thông qua phương thức rủi ro (như email). Vui lòng xóa nó ngay lập tức khi bạn đã sử dụng xong." }, "encExportKeyWarningDesc": { - "message": "File xuất mã hóa dữ liệu xủa bạn bằng khóa giải mã. Nếu bạn thay đổi khóa giải mã, bạn nên xuất file lại vì bạn sẽ không thể giải mã file này." + "message": "Quá trình xuất này mã hóa dữ liệu của bạn bằng khóa mã hóa của tài khoản. Nếu bạn đã từng thay đổi khóa mã hóa của tài khoản, bạn cần xuất lại vì bạn sẽ không thể giải mã tệp xuất này." }, "encExportAccountWarningDesc": { - "message": "Account encryption keys are unique to each Bitwarden user account, so you can't import an encrypted export into a different account." + "message": "Khóa mã hóa tài khoản là duy nhất cho mỗi tài khoản Bitwarden, vì vậy bạn không thể nhập tệp xuất được mã hóa vào một tài khoản khác." }, "noOrganizationsList": { - "message": "You do not belong to any organizations. Organizations allow you to securely share items with other users." + "message": "Bạn chưa thuộc tổ chức nào. Tổ chức sẽ cho phép bạn chia sẻ các mục với người dùng khác một cách bảo mật." }, "noCollectionsInList": { - "message": "Không có bộ sưu tập nào để liệt kê." + "message": "Không có bộ sưu tập nào." }, "ownership": { - "message": "Quyền sở hữu" + "message": "Sở hữu" }, "whoOwnsThisItem": { "message": "Ai sở hữu mục này?" @@ -1447,7 +1492,7 @@ "description": "ex. A weak password. Scale: Weak -> Good -> Strong" }, "weakMasterPassword": { - "message": "Weak Master Password" + "message": "Mật khẩu chính Yếu" }, "weakMasterPasswordDesc": { "message": "Mật khẩu chính bạn vừa chọn có vẻ yếu. Bạn nên chọn mật khẩu chính (hoặc cụm từ mật khẩu) mạnh để bảo vệ đúng cách tài khoản Bitwarden của bạn. Bạn có thực sự muốn dùng mật khẩu chính này?" @@ -1469,37 +1514,46 @@ "message": "Mã PIN không hợp lệ." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "Mã PIN bị gõ sai quá nhiều lần. Đang đăng xuất." }, "unlockWithWindowsHello": { "message": "Mở khóa với Windows Hello" }, "additionalWindowsHelloSettings": { - "message": "Additional Windows Hello settings" + "message": "Cài đặt Windows Hello bổ sung" + }, + "unlockWithPolkit": { + "message": "Mở khóa bằng bảo mật hệ thống" }, "windowsHelloConsentMessage": { "message": "Xác minh cho Bitwarden." }, + "polkitConsentMessage": { + "message": "Xác thực để mở khóa Bitwarden." + }, "unlockWithTouchId": { "message": "Mở khóa với Touch ID" }, "additionalTouchIdSettings": { - "message": "Additional Touch ID settings" + "message": "Cài đặt Touch ID bổ sung" }, "touchIdConsentMessage": { - "message": "Xác minh cho Bitwarden." + "message": "mở khoá kho của bạn" }, "autoPromptWindowsHello": { "message": "Yêu cầu xác minh Windows Hello khi mở" }, + "autoPromptPolkit": { + "message": "Hỏi xác thực hệ thống khi khởi chạy" + }, "autoPromptTouchId": { "message": "Yêu cầu xác minh Touch ID khi mở" }, "requirePasswordOnStart": { - "message": "Require password or PIN on app start" + "message": "Yêu cầu mật khẩu hoặc mã PIN khi mở" }, "recommendedForSecurity": { - "message": "Recommended for security." + "message": "Đề xuất để tăng cường bảo mật." }, "lockWithMasterPassOnRestart": { "message": "Khóa với mật khẩu chính khi khởi động lại" @@ -1523,7 +1577,7 @@ "message": "Tuỳ chỉnh" }, "enableMenuBar": { - "message": "Bật biểu tượng thanh menu" + "message": "Hiển thị biểu tượng thanh menu" }, "enableMenuBarDesc": { "message": "Luôn hiển biểu tượng trên thanh menu." @@ -1563,10 +1617,10 @@ "message": "Tạo bản sao" }, "passwordGeneratorPolicyInEffect": { - "message": "Có một hoặc vài chính sách của tổ chức đang làm ảnh hưởng đến cài đặt tạo mật khẩu của bạn." + "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." }, "vaultTimeoutAction": { - "message": "Hành Động Khi Hết Thời Gian Chờ" + "message": "Khi hết thời gian mở kho" }, "vaultTimeoutActionLockDesc": { "message": "Kho bị khóa sẽ yêu cầu bạn nhập lại mật khẩu chính để có thể truy cập." @@ -1575,7 +1629,7 @@ "message": "Kho bị đăng xuất sẽ yêu cầu bạn xác thực lại để có thể truy cập." }, "unlockMethodNeededToChangeTimeoutActionDesc": { - "message": "Set up an unlock method to change your vault timeout action." + "message": "Thiết lập khóa khi hết thời gian chờ kho." }, "lock": { "message": "Khóa", @@ -1586,7 +1640,7 @@ "description": "Noun: a special folder to hold deleted items" }, "searchTrash": { - "message": "Tìm kiếm thùng rác" + "message": "Tìm kiếm trong thùng rác" }, "permanentlyDeleteItem": { "message": "Xoá vĩnh viễn mục" @@ -1601,10 +1655,10 @@ "message": "Mục đã được khôi phục" }, "permanentlyDelete": { - "message": "Xóa Vĩnh Viễn" + "message": "Xoá vĩnh viễn" }, "vaultTimeoutLogOutConfirmation": { - "message": "Đăng xuất sẽ xóa tất các truy cập vào kho của bạn và yêu cầu xác thực trực tuyến sau khi khoảng thời gian chờ hết. Bạn có chắc bạn muốn dùng cài đặt này?" + "message": "Đăng xuất sẽ xóa tất cả quyền truy cập vào kho của bạn và yêu cầu xác minh trực tuyến sau khi hết thời gian chờ. Bạn có chắc chắn muốn sử dụng cài đặt này không?" }, "vaultTimeoutLogOutConfirmationTitle": { "message": "Xác nhận hành động khi hết thời gian chờ" @@ -1616,19 +1670,19 @@ "message": "Thiết lập mật khẩu chính" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "Quyền tổ chức của bạn đã được cập nhật, yêu cầu bạn đặt mật khẩu chính.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "Tổ chức của bạn yêu cầu bạn đặt mật khẩu chính.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { - "message": "Verification required", + "message": "Yêu cầu xác minh", "description": "Default title for the user verification dialog." }, "currentMasterPass": { - "message": "Current master password" + "message": "Mật khẩu chính hiện tại" }, "newMasterPass": { "message": "Mật khẩu chính mới" @@ -1637,10 +1691,10 @@ "message": "Xác nhận mật khẩu chính mới" }, "masterPasswordPolicyInEffect": { - "message": "One or more organization policies require your master password to meet the following requirements:" + "message": "Các chính sách của tổ chức yêu cầu mật khẩu chính của bạn phải:" }, "policyInEffectMinComplexity": { - "message": "Minimum complexity score of $SCORE$", + "message": "Độ mạnh tối thiểu $SCORE$", "placeholders": { "score": { "content": "$1", @@ -1649,7 +1703,7 @@ } }, "policyInEffectMinLength": { - "message": "Minimum length of $LENGTH$", + "message": "Độ dài tối thiểu là $LENGTH$", "placeholders": { "length": { "content": "$1", @@ -1661,7 +1715,7 @@ "message": "Có chứa một hay nhiều ký tự viết hoa" }, "policyInEffectLowercase": { - "message": "Chứa một hoặc nhiều kí tự viết thường" + "message": "Có chứa một hay nhiều ký tự thường" }, "policyInEffectNumbers": { "message": "Có chứa một hay nhiều số" @@ -1676,25 +1730,25 @@ } }, "masterPasswordPolicyRequirementsNotMet": { - "message": "Your new master password does not meet the policy requirements." + "message": "Mật khẩu chính bạn chọn không đáp ứng yêu cầu." }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Nhận đề xuất, thông báo và cơ hội nghiên cứu từ Bitwarden trong hộp thư đến của bạn." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "Hủy đăng ký" }, "atAnyTime": { - "message": "at any time." + "message": "bất cứ lúc nào." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Nếu tiếp tục, bạn đồng ý" }, "and": { - "message": "and" + "message": "và" }, "acceptPolicies": { - "message": "By checking this box you agree to the following:" + "message": "Bạn đồng ý với những điều sau khi nhấn chọn ô này:" }, "acceptPoliciesRequired": { "message": "Điều khoản sử dụng và chính sách quyền riêng tư chưa được đồng ý." @@ -1703,7 +1757,7 @@ "message": "Cho phép tích hợp với trình duyệt" }, "enableBrowserIntegrationDesc": { - "message": "Used for biometrics in browser." + "message": "Sử dụng để xác thực sinh trắc học trên trình duyệt." }, "enableDuckDuckGoBrowserIntegration": { "message": "Cho phép tích hợp trình duyệt DuckDuckGo" @@ -1712,13 +1766,13 @@ "message": "Sử dụng kho Bitwarden của bạn khi tìm kiếm bằng DuckDuckGo." }, "browserIntegrationUnsupportedTitle": { - "message": "Không hỗ trợ tích hợp trình duyệt" + "message": "Tích hợp trình duyệt không được hỗ trợ" }, "browserIntegrationErrorTitle": { - "message": "Error enabling browser integration" + "message": "Lỗi khi bật tích hợp trình duyệt" }, "browserIntegrationErrorDesc": { - "message": "An error has occurred while enabling browser integration." + "message": "Đã xảy ra lỗi khi bật tích hợp với trình duyệt." }, "browserIntegrationMasOnlyDesc": { "message": "Rất tiếc, tính năng tích hợp trình duyệt hiện chỉ được hỗ trợ trong phiên bản App Store trên Mac." @@ -1733,25 +1787,25 @@ "message": "Yêu cầu xác minh để tích hợp trình duyệt" }, "enableBrowserIntegrationFingerprintDesc": { - "message": "Add an additional layer of security by requiring fingerprint phrase confirmation when establishing a link between your desktop and browser. This requires user action and verification each time a connection is created." + "message": "Tăng cường bảo mật bằng cách yêu cầu xác nhận cụm vân tay khi kết nối máy tính với trình duyệt. Việc này yêu cầu bạn phải thực hiện thao tác xác minh mỗi khi tạo kết nối." }, "enableHardwareAcceleration": { - "message": "Use hardware acceleration" + "message": "Dùng tính năng tăng tốc phần cứng" }, "enableHardwareAccelerationDesc": { - "message": "By default this setting is ON. Turn OFF only if you experience graphical issues. Restart is required." + "message": "Mặc định cài đặt này được bật. Chỉ tắt khi gặp sự cố đồ họa. Cần khởi động lại." }, "approve": { - "message": "Chấp nhận" + "message": "Phê duyệt" }, "verifyBrowserTitle": { "message": "Xác minh kết nối trình duyệt" }, "verifyBrowserDesc": { - "message": "Please ensure the shown fingerprint is identical to the fingerprint showed in the browser extension." + "message": "Vui lòng đảm bảo cụm vân tay hiển thị giống hệt với cụm vân tay được hiển thị trong tiện ích mở rộng trình duyệt." }, "verifyNativeMessagingConnectionTitle": { - "message": "$APPID$ wants to connect to Bitwarden", + "message": "$APPID$ muốn kết nối với Bitwarden", "placeholders": { "appid": { "content": "$1", @@ -1769,22 +1823,28 @@ "message": "Sinh trắc học chưa được thiết lập" }, "biometricsNotEnabledDesc": { - "message": "Browser biometrics requires desktop biometrics to be set up in the settings first." + "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": "Thiết lập sinh trắc tự động không khả dụng" + }, + "biometricsManualSetupDesc": { + "message": "Do phương pháp cài đặt, sinh trắc học không thể được bật tự động. Bạn có muốn mở hướng dẫn cách thực hiện thủ công?" }, "personalOwnershipSubmitError": { - "message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections." + "message": "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." }, "hintEqualsPassword": { "message": "Gợi ý mật khẩu không được trùng với mật khẩu của bạn." }, "personalOwnershipPolicyInEffect": { - "message": "An organization policy is affecting your ownership options." + "message": "Chính sách của tổ chức đang ảnh hưởng đến các tùy chọn quyền sở hữu của bạn." }, "personalOwnershipPolicyInEffectImports": { - "message": "An organization policy has blocked importing items into your individual vault." + "message": "Chính sách của tổ chức đã chặn việc nhập các mục vào kho cá nhân của bạn." }, "allSends": { - "message": "Toàn bộ Send", + "message": "Tất cả mục Gửi", "description": "'Sends' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTypeFile": { @@ -1794,15 +1854,15 @@ "message": "Văn bản" }, "searchSends": { - "message": "Tìm kiếm Send", + "message": "Tìm kiếm mục Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editSend": { - "message": "Chỉnh sửa Send", + "message": "Sửa mục Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "myVault": { - "message": "Hầm của tôi" + "message": "Kho của tôi" }, "text": { "message": "Văn bản" @@ -1811,14 +1871,14 @@ "message": "Ngày xóa" }, "deletionDateDesc": { - "message": "Send sẽ được xóa vĩnh viễn vào ngày và giờ được chỉ định.", + "message": "Mục Gửi sẽ được xóa vĩnh viễn vào ngày và giờ chỉ định.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "expirationDate": { - "message": "Ngày Hết Hạn" + "message": "Ngày hết hạn" }, "expirationDateDesc": { - "message": "Nếu được thiết lập, truy cập vào Send này sẽ hết hạn vào ngày và giờ được chỉ định.", + "message": "Nếu được thiết lập, mục Gửi này sẽ hết hạn vào ngày và giờ được chỉ định.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "maxAccessCount": { @@ -1826,57 +1886,57 @@ "description": "This text will be displayed after a Send has been accessed the maximum amount of times." }, "maxAccessCountDesc": { - "message": "Nếu được thiết lập, khi đã đạt tới số lượng truy cập tối đa, người dùng sẽ không thể truy cập Send này nữa.", + "message": "Nếu được thiết lập, khi đã đạt tới số lượng truy cập tối đa, người dùng sẽ không thể truy cập mục Gửi này nữa.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "currentAccessCount": { "message": "Số lượng truy cập hiện tại" }, "disableSend": { - "message": "Deactivate this Send so that no one can access it.", + "message": "Vô hiệu hoá mục Gửi này để không ai có thể truy cập nó.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendPasswordDesc": { - "message": "Optionally require a password for users to access this Send.", + "message": "Yêu cầu nhập mật khẩu khi người dùng truy cập vào phần Gửi này.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { - "message": "Private notes about this Send.", + "message": "Ghi chú riêng tư về mục Gửi này.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLink": { - "message": "Gửi liên kết", + "message": "Liên kết Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLinkLabel": { - "message": "Gửi liên kết", + "message": "Liên kết Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "textHiddenByDefault": { - "message": "When accessing the Send, hide the text by default", + "message": "Khi truy cập vào phần Gửi, văn bản sẽ được ẩn theo mặc định", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createdSend": { - "message": "Đã tạo Send", + "message": "Đã tạo mục Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editedSend": { - "message": "Đã chỉnh sửa Send", + "message": "Đã lưu mục Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deletedSend": { - "message": "Đã xóa Send", + "message": "Đã xóa mục Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "newPassword": { "message": "Mật khẩu mới" }, "whatTypeOfSend": { - "message": "Đây là loại Send gì?", + "message": "Đây là kiểu Gửi gì?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "createSend": { - "message": "Tạo Send", + "message": "Mục Gửi mới", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendTextDesc": { @@ -1901,18 +1961,18 @@ "message": "Tùy chỉnh" }, "deleteSendConfirmation": { - "message": "Bạn có chắc chắn muốn xóa Send này?", + "message": "Bạn có chắc muốn mục Gửi này?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "copySendLinkToClipboard": { - "message": "Sao chép liên kết tới Khay nhớ tạm", + "message": "Sao chép liên kết tới khay nhớ tạm", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "copySendLinkOnSave": { - "message": "Copy the link to share this Send to my clipboard upon save." + "message": "Sao chép liên kết chia sẻ của mục Gửi này tới khay nhớ tạm khi lưu." }, "sendDisabled": { - "message": "Đã tắt Send", + "message": "Đã loại bỏ Gửi", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDisabledWarning": { @@ -1926,16 +1986,16 @@ "message": "Đã tắt" }, "removePassword": { - "message": "Remove password" + "message": "Xóa mật khẩu" }, "removedPassword": { - "message": "Password removed" + "message": "Đã xóa mật khẩu" }, "removePasswordConfirmation": { - "message": "Are you sure you want to remove the password?" + "message": "Bạn có chắc muốn xóa mật khẩu này?" }, "maxAccessCountReached": { - "message": "Đã đạt đến số lượng truy cập tối đa" + "message": "Đã vượt số lần truy cập tối đa" }, "expired": { "message": "Đã hết hạn" @@ -1950,10 +2010,13 @@ "message": "Ẩn địa chỉ email của tôi khỏi người nhận." }, "sendOptionsPolicyInEffect": { - "message": "Có một hoặc vài chính sách của tổ chức đang làm ảnh hưởng đến cài đặt tạo mật khẩu của bạn." + "message": "Các chính sách của tổ chức đang ảnh hưởng đến tùy chọn Gửi của bạn." }, "emailVerificationRequired": { - "message": "Yêu cầu xác nhận danh tính qua Email" + "message": "Yêu cầu xác nhận danh tính qua email" + }, + "emailVerifiedV2": { + "message": "Email đã xác minh" }, "emailVerificationRequiredDesc": { "message": "Bạn phải xác minh email của mình để sử dụng tính năng này." @@ -1977,43 +2040,46 @@ "message": "Mật khẩu chính của bạn gần đây đã được thay đổi bởi một quản trị viên trong tổ chức của bạn. Để truy cập Kho, bạn phải cập nhật nó ngay bây giờ. Tiếp tục sẽ đăng xuất bạn khỏi phiên hiện tại của bạn, yêu cầu bạn đă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 hoạt động trong tối đa một giờ." }, "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." + "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": "Tổ chức của bạn đã vô hiệu hóa mã hóa bằng thiết bị đáng tin cậy. Vui lòng đặt mật khẩu chính để truy cập Kho của bạn." }, "tryAgain": { - "message": "Try again" + "message": "Thử lại" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "Cần xác minh cho thao tác này. Hãy đặt mã PIN để tiếp tục." }, "setPin": { - "message": "Set PIN" + "message": "Thiết lập mã PIN" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Xác thực bằng sinh trắc học" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Đang chờ xác nhận" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "Không thể hoàn tất sinh trắc học." }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "Cần một phương pháp khác?" }, "useMasterPassword": { - "message": "Use master password" + "message": "Dùng mật khẩu chính" }, "usePin": { - "message": "Use PIN" + "message": "Dùng mã PIN" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Dùng sinh trắc học" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "Nhập mã xác minh được gửi đến email của bạn." }, "resendCode": { - "message": "Resend code" + "message": "Gửi lại mã" }, "hours": { "message": "Giờ" @@ -2022,7 +2088,7 @@ "message": "Phút" }, "vaultTimeoutPolicyInEffect": { - "message": "Chính sách tổ chức của bạn đang ảnh hưởng đến thời gian chờ Kho của bạn. Thời gian chờ kho tối đa được phép là $HOURS$ giờ và $MINUTES$ phút", + "message": "Tổ chức của bạn đã đặt thời gian mở kho tối đa là $HOURS$ giờ và $MINUTES$ phút.", "placeholders": { "hours": { "content": "$1", @@ -2035,7 +2101,7 @@ } }, "vaultTimeoutPolicyWithActionInEffect": { - "message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", + "message": "Tổ chức của bạn đang ảnh hưởng đến thời gian mở kho. Thời gian mở kho tối đa là $HOURS$ giờ và $MINUTES$ phút. Kho sẽ $ACTION$ sau khi hết thời gian mở kho.", "placeholders": { "hours": { "content": "$1", @@ -2052,7 +2118,7 @@ } }, "vaultTimeoutActionPolicyInEffect": { - "message": "Your organization policies have set your vault timeout action to $ACTION$.", + "message": "Tổ chức của bạn sẽ $ACTION$ sau khi hết thời gian mở kho.", "placeholders": { "action": { "content": "$1", @@ -2061,19 +2127,22 @@ } }, "vaultTimeoutTooLarge": { - "message": "Thời gian chờ Kho của bạn vượt quá các hạn chế do tổ chức của bạn đặt ra." + "message": "Thời gian mở kho vượt quá giới hạn do tổ chức của bạn đặt ra." + }, + "inviteAccepted": { + "message": "Lời mời được chấp nhận" }, "resetPasswordPolicyAutoEnroll": { "message": "Đăng ký tự động" }, "resetPasswordAutoEnrollInviteWarning": { - "message": "This organization has an enterprise policy that will automatically enroll you in password reset. Enrollment will allow organization administrators to change your master password." + "message": "Tổ chức này có chính sách doanh nghiệp sẽ tự động đặt lại mật khẩu chính cho bạn. Đăng ký sẽ cho phép quản trị viên tổ chức thay đổi mật khẩu chính của bạn." }, "vaultExportDisabled": { - "message": "Vault export removed" + "message": "Đã xoá xuất kho" }, "personalVaultExportPolicyInEffect": { - "message": "One or more organization policies prevents you from exporting your personal vault." + "message": "Các chính sách của tổ chức ngăn cản bạn xuất kho lưu trữ cá nhân của mình." }, "addAccount": { "message": "Thêm tài khoản" @@ -2085,7 +2154,7 @@ "message": "Đã xóa mật khẩu chính" }, "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", + "message": "$ORGANIZATION$ đang sử dụng SSO với khóa máy chủ tự lưu trữ. Mật khẩu chính không còn cần để đăng nhập cho các thành viên của tổ chức này.", "placeholders": { "organization": { "content": "$1", @@ -2097,31 +2166,31 @@ "message": "Rời khỏi tổ chức" }, "leaveOrganizationConfirmation": { - "message": "Are you sure you want to leave this organization?" + "message": "Bạn có chắc chắn muốn rời tổ chức này không?" }, "leftOrganization": { - "message": "You have left the organization." + "message": "Bạn đã rời khỏi tổ chức." }, "ssoKeyConnectorError": { - "message": "Key connector error: make sure key connector is available and working correctly." + "message": "Lỗi kết nối khóa: hãy đảm bảo kết nối khóa khả dụng và hoạt động chính xác." }, "lockAllVaults": { - "message": "Lock all vaults" + "message": "Khoá tất cả kho" }, "accountLimitReached": { - "message": "No more than 5 accounts may be logged in at the same time." + "message": "Bạn chỉ có thể đăng nhập tối đa 5 tài khoản cùng lúc." }, "accountPreferences": { "message": "Tuỳ chỉnh" }, "appPreferences": { - "message": "App settings (all accounts)" + "message": "Cài đặt ứng dụng (tất cả tài khoản)" }, "accountSwitcherLimitReached": { - "message": "Account limit reached. Log out of an account to add another." + "message": "Số lượng tài khoản đã đạt giới hạn. Đăng xuất khỏi một tài khoản để thêm tài khoản khác." }, "settingsTitle": { - "message": "App settings for $EMAIL$", + "message": "Cài đặt ứng dụng cho $EMAIL$", "placeholders": { "email": { "content": "$1", @@ -2133,19 +2202,19 @@ "message": "Chuyển tài khoản" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Bạn đã có tài khoản?" }, "options": { "message": "Tùy chọn" }, "sessionTimeout": { - "message": "Your session has timed out. Please go back and try logging in again." + "message": "Phiên đăng nhập của bạn đã hết hạn. Vui lòng quay trở lại và thử đăng nhập lại." }, "exportingPersonalVaultTitle": { - "message": "Exporting individual vault" + "message": "Đang xuất dữ liệu kho cá nhân" }, "exportingIndividualVaultDescription": { - "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.", + "message": "Chỉ dữ liệu trong kho cá nhân liên kết với $EMAIL$ mới được xuất. Không bao gồm \ncác dữ liệu trong kho tổ chức. Chỉ thông tin mục kho mới được xuất, sẽ không có các tệp đính kèm.", "placeholders": { "email": { "content": "$1", @@ -2154,10 +2223,10 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "Đang xuất dữ liệu kho tổ chức" }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Chỉ dữ liệu trong kho tổ chức $ORGANIZATION$ mới được xuất. Các kho cá nhân hoặc của tổ chức khác sẽ không được bao gồm.", "placeholders": { "organization": { "content": "$1", @@ -2172,7 +2241,7 @@ "message": "Đã mở khóa" }, "generator": { - "message": "Tạo" + "message": "Trình tạo" }, "whatWouldYouLikeToGenerate": { "message": "Bạn muốn tạo gì?" @@ -2181,26 +2250,26 @@ "message": "Loại mật khẩu" }, "regenerateUsername": { - "message": "Regenerate username" + "message": "Tạo lại tên người dùng" }, "generateUsername": { "message": "Tạo tên tài khoản" }, "usernameType": { - "message": "Username type" + "message": "Loại tên người dùng" }, "plusAddressedEmail": { - "message": "Plus addressed email", + "message": "Địa chỉ email có hậu tố", "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" }, "plusAddressedEmailDesc": { - "message": "Use your email provider's sub-addressing capabilities." + "message": "Sử dụng khả năng địa chỉ phụ của nhà cung cấp dịch vụ mail của bạn." }, "catchallEmail": { "message": "Catch-all email" }, "catchallEmailDesc": { - "message": "Use your domain's configured catch-all inbox." + "message": "Sử dụng hộp thư bạn đã thiết lập để nhận tất cả email gửi đến tên miền của bạn." }, "random": { "message": "Ngẫu nhiên" @@ -2221,16 +2290,16 @@ "message": "Tìm tổ chức" }, "searchMyVault": { - "message": "Search my vault" + "message": "Tìm kiếm trong kho" }, "forwardedEmail": { - "message": "Forwarded email alias" + "message": "Đã chuyển tiếp bí danh email" }, "forwardedEmailDesc": { - "message": "Generate an email alias with an external forwarding service." + "message": "Tạo bí danh email với dịch vụ chuyển tiếp bên ngoài." }, "forwarderError": { - "message": "$SERVICENAME$ error: $ERRORMESSAGE$", + "message": "Lỗi $SERVICENAME$: $ERRORMESSAGE$", "description": "Reports an error returned by a forwarding service to the user.", "placeholders": { "servicename": { @@ -2244,11 +2313,11 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "Được tạo bởi Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "Trang web: $WEBSITE$. Được tạo bởi Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -2258,7 +2327,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "Khoá API $SERVICENAME$ không hợp lệ", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -2268,7 +2337,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "Khoá API $SERVICENAME$ không hợp lệ: $ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -2282,7 +2351,7 @@ } }, "forwarderNoAccountId": { - "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "message": "Không thể lấy ID tài khoản email ẩn từ $SERVICENAME$.", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -2292,7 +2361,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "Tên miền $SERVICENAME$ không hợp lệ.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -2302,7 +2371,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "Địa chỉ $SERVICENAME$ không hợp lệ.", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -2312,7 +2381,7 @@ } }, "forwarderUnknownError": { - "message": "Unknown $SERVICENAME$ error occurred.", + "message": "$SERVICENAME$ đã xảy ra lỗi không xác định.", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -2322,7 +2391,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Unknown forwarder: '$SERVICENAME$'.", + "message": "Người chuyển tiếp không xác định: '$SERVICENAME$'.", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -2332,47 +2401,47 @@ } }, "hostname": { - "message": "Hostname", + "message": "Tên máy chủ", "description": "Part of a URL." }, "apiAccessToken": { - "message": "API Access Token" + "message": "Khoá truy cập API" }, "apiKey": { "message": "Khóa API" }, "premiumSubcriptionRequired": { - "message": "Premium subscription required" + "message": "Yêu cầu đăng ký gói Premium" }, "organizationIsDisabled": { - "message": "Organization suspended" + "message": "Tổ chức đã ngưng hoạt động" }, "disabledOrganizationFilterError": { - "message": "Items in suspended organizations cannot be accessed. Contact your organization owner for assistance." + "message": "Không thể truy cập các mục trong tổ chức đã ngưng hoạt động. Hãy liên hệ với chủ sở hữu tổ chức để được hỗ trợ." }, "neverLockWarning": { - "message": "Are you sure you want to use the \"Never\" option? Setting your lock options to \"Never\" stores your vault's encryption key on your device. If you use this option you should ensure that you keep your device properly protected." + "message": "Bạn có chắc chắn muốn chọn \"Không bao giờ\" không? Lựa chọn này sẽ lưu khóa mã hóa kho của bạn trực tiếp trên thiết bị. Hãy nhớ bảo vệ thiết bị của bạn thật cẩn thận nếu bạn chọn tùy chọn này." }, "vault": { - "message": "Vault" + "message": "Kho mật khẩu" }, "loginWithMasterPassword": { - "message": "Log in with master password" + "message": "Đăng nhập bằng mật khẩu chính" }, "loggingInAs": { - "message": "Logging in as" + "message": "Đang đăng nhập như" }, "rememberEmail": { - "message": "Remember email" + "message": "Ghi nhớ email" }, "notYou": { - "message": "Not you?" + "message": "Không phải bạn?" }, "newAroundHere": { - "message": "New around here?" + "message": "Bạn mới tới đây sao?" }, "loggingInTo": { - "message": "Logging in to $DOMAIN$", + "message": "Đang đăng nhập vào $DOMAIN$", "placeholders": { "domain": { "content": "$1", @@ -2381,38 +2450,38 @@ } }, "logInWithAnotherDevice": { - "message": "Log in with another device" + "message": "Đăng nhập bằng thiết bị khác" }, "loginInitiated": { - "message": "Login initiated" + "message": "Bắt đầu đăng nhập" }, "notificationSentDevice": { - "message": "A notification has been sent to your device." + "message": "Một thông báo đã được gửi đến thiết bị của bạn." }, "fingerprintMatchInfo": { - "message": "Please make sure your vault is unlocked and Fingerprint phrase matches the other device." + "message": "Vui lòng đảm bảo rằng bạn đã mở khoá kho và cụm vân tay khớp trên thiết bị khác." }, "fingerprintPhraseHeader": { - "message": "Fingerprint phrase" + "message": "Cụm vân tay" }, "needAnotherOption": { - "message": "Log in with device must be set up in the settings of the Bitwarden app. Need another option?" + "message": "Đăng nhập bằng thiết bị phải được thiết lập trong cài đặt của ứng dụng Bitwarden. Dùng cách khác?" }, "viewAllLoginOptions": { - "message": "View all login options" + "message": "Xem tất cả tùy chọn đăng nhập" }, "resendNotification": { - "message": "Resend notification" + "message": "Gửi lại thông báo" }, "toggleCharacterCount": { - "message": "Toggle character count", + "message": "Bật tắt đếm kí tự", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, "areYouTryingtoLogin": { - "message": "Are you trying to log in?" + "message": "Bạn đang cố đăng nhập?" }, "logInAttemptBy": { - "message": "Login attempt by $EMAIL$", + "message": "Nỗ lực đăng nhập bằng $EMAIL$", "placeholders": { "email": { "content": "$1", @@ -2421,22 +2490,22 @@ } }, "deviceType": { - "message": "Device Type" + "message": "Loại thiết bị" }, "ipAddress": { - "message": "IP Address" + "message": "Địa chỉ IP" }, "time": { - "message": "Time" + "message": "Thời Gian" }, "confirmLogIn": { - "message": "Confirm login" + "message": "Xác nhận đăng nhập" }, "denyLogIn": { - "message": "Deny login" + "message": "Từ chối đăng nhập" }, "logInConfirmedForEmailOnDevice": { - "message": "Login confirmed for $EMAIL$ on $DEVICE$", + "message": "Đã xác nhận đăng nhập cho $EMAIL$ trên $DEVICE$", "placeholders": { "email": { "content": "$1", @@ -2449,13 +2518,13 @@ } }, "youDeniedALogInAttemptFromAnotherDevice": { - "message": "You denied a login attempt from another device. If this really was you, try to log in with the device again." + "message": "Bạn đã từ chối đăng nhập từ thiết bị khác. Nếu đó là bạn, hãy thử đăng nhập lại bằng thiết bị đó." }, "justNow": { - "message": "Just now" + "message": "Vừa xong" }, "requestedXMinutesAgo": { - "message": "Requested $MINUTES$ minutes ago", + "message": "Đã yêu cầu $MINUTES$ phút trước", "placeholders": { "minutes": { "content": "$1", @@ -2464,13 +2533,13 @@ } }, "loginRequestHasAlreadyExpired": { - "message": "Login request has already expired." + "message": "Yêu cầu đăng nhập đã hết hạn." }, "thisRequestIsNoLongerValid": { - "message": "This request is no longer valid." + "message": "Yêu cầu này không còn hiệu lực." }, "confirmLoginAtemptForMail": { - "message": "Confirm login attempt for $EMAIL$", + "message": "Phê duyệt đăng nhập cho $EMAIL$", "placeholders": { "email": { "content": "$1", @@ -2479,58 +2548,58 @@ } }, "logInRequested": { - "message": "Log in requested" + "message": "Yêu cầu đăng nhập" }, "creatingAccountOn": { - "message": "Creating account on" + "message": "Đang tạo tài khoản trên" }, "checkYourEmail": { - "message": "Check your email" + "message": "Kiểm tra email của bạn" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "Nhấp vào liên kết trong email được gửi đến" }, "andContinueCreatingYourAccount": { - "message": "and continue creating your account." + "message": "và tiếp tục tạo tài khoản của bạn." }, "noEmail": { - "message": "No email?" + "message": "Không có email?" }, "goBack": { - "message": "Go back" + "message": "Quay lại" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "để chỉnh sửa địa chỉ email của bạn." }, "exposedMasterPassword": { - "message": "Exposed Master Password" + "message": "Mật khẩu chính bị lộ" }, "exposedMasterPasswordDesc": { - "message": "Password found in a data breach. Use a unique password to protect your account. Are you sure you want to use an exposed password?" + "message": "Mật khẩu này đã bị rò rỉ trong một vụ tấn công dữ liệu. Dùng mật khẩu mới và an toàn để bảo vệ tài khoản bạn. Bạn có chắc muốn sử dụng mật khẩu đã bị rò rỉ?" }, "weakAndExposedMasterPassword": { - "message": "Weak and Exposed Master Password" + "message": "Mật khẩu chính yếu và bị lộ" }, "weakAndBreachedMasterPasswordDesc": { - "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" + "message": "Mật khẩu yếu này đã bị rò rỉ trong một vụ tấn công dữ liệu. Dùng mật khẩu mới và an toàn để bảo vệ tài khoản bạn. Bạn có chắc muốn sử dụng mật khẩu đã bị rò rỉ?" }, "checkForBreaches": { - "message": "Check known data breaches for this password" + "message": "Kiểm tra mật khẩu có lộ trong các vụ rò rỉ dữ liệu hay không" }, "important": { - "message": "Important:" + "message": "Quan trọng:" }, "accessTokenUnableToBeDecrypted": { - "message": "You have been logged out because your access token could not be decrypted. Please log in again to resolve this issue." + "message": "Bạn đã bị đăng xuất do khoá truy cập của bạn không thể giải mã. Vui lòng đăng nhập lại để khắc phục sự cố này." }, "refreshTokenSecureStorageRetrievalFailure": { - "message": "You have been logged out because your refresh token could not be retrieved. Please log in again to resolve this issue." + "message": "Bạn đã bị đăng xuất do khoá truy cập của bạn không thể truy xuất. Vui lòng đăng nhập lại để khắc phục sự cố này." }, "masterPasswordHint": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "Mật khẩu chính của bạn không thể phục hồi nếu bạn quên nó!" }, "characterMinimum": { - "message": "$LENGTH$ character minimum", + "message": "Tối thiểu $LENGTH$ ký tự", "placeholders": { "length": { "content": "$1", @@ -2539,83 +2608,83 @@ } }, "windowsBiometricUpdateWarning": { - "message": "Bitwarden recommends updating your biometric settings to require your master password (or PIN) on the first unlock. Would you like to update your settings now?" + "message": "Bitwarden khuyên bạn nên cập nhật cài đặt sinh trắc học để yêu cầu mật khẩu chính (hoặc mã PIN) trong lần mở khóa đầu tiên. Bạn có muốn cập nhật cài đặt của mình bây giờ không?" }, "windowsBiometricUpdateWarningTitle": { - "message": "Recommended Settings Update" + "message": "Cập nhật cài đặt được đề xuất" }, "deviceApprovalRequired": { - "message": "Device approval required. Select an approval option below:" + "message": "Yêu cầu phê duyệt thiết bị. Chọn một tuỳ chọn phê duyệt bên dưới:" }, "rememberThisDevice": { - "message": "Remember this device" + "message": "Lưu thiết bị này" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "Bỏ chọn nếu sử dụng thiết bị công cộng" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Phê duyệt bằng thiết bị khác" }, "requestAdminApproval": { - "message": "Request admin approval" + "message": "Yêu cầu quản trị viên phê duyệt" }, "approveWithMasterPassword": { - "message": "Approve with master password" + "message": "Phê duyệt bằng mật khẩu chính" }, "region": { - "message": "Region" + "message": "Khu vực" }, "ssoIdentifierRequired": { - "message": "Organization SSO identifier is required." + "message": "Cần có mã định danh SSO của tổ chức." }, "eu": { - "message": "EU", + "message": "Châu Âu", "description": "European Union" }, "loggingInOn": { - "message": "Logging in on" + "message": "Đang đăng nhập vào" }, "selfHostedServer": { - "message": "self-hosted" + "message": "tự lưu trữ" }, "accessDenied": { - "message": "Access denied. You do not have permission to view this page." + "message": "Truy cập bị từ chối. Bạn không có quyền xem trang này." }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Tạo tài khoản thành công!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "Yêu cầu quản trị viên phê duyệt" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "Yêu cầu của bạn đã được gửi đến quản trị viên." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Bạn sẽ có thông báo nếu được phê duyệt." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Không thể đăng nhập?" }, "loginApproved": { - "message": "Login approved" + "message": "Lượt đăng nhập đã duyệt" }, "userEmailMissing": { - "message": "User email missing" + "message": "Thiếu email người dùng" }, "deviceTrusted": { - "message": "Device trusted" + "message": "Thiết bị tin cậy" }, "inputRequired": { - "message": "Input is required." + "message": "Trường này là bắt buộc." }, "required": { - "message": "required" + "message": "bắt buộc" }, "search": { - "message": "Search" + "message": "Tìm kiếm" }, "inputMinLength": { - "message": "Input must be at least $COUNT$ characters long.", + "message": "Giá trị nhập vào phải ít nhất $COUNT$ ký tự.", "placeholders": { "count": { "content": "$1", @@ -2624,7 +2693,7 @@ } }, "inputMaxLength": { - "message": "Input must not exceed $COUNT$ characters in length.", + "message": "Giá trị nhập vào không được vượt quá $COUNT$ ký tự.", "placeholders": { "count": { "content": "$1", @@ -2633,7 +2702,7 @@ } }, "inputForbiddenCharacters": { - "message": "The following characters are not allowed: $CHARACTERS$", + "message": "Các ký tự sau không được phép sử dụng: $CHARACTERS$", "placeholders": { "characters": { "content": "$1", @@ -2642,7 +2711,7 @@ } }, "inputMinValue": { - "message": "Input value must be at least $MIN$.", + "message": "Giá trị nhập vào phải ít nhất $MIN$.", "placeholders": { "min": { "content": "$1", @@ -2651,7 +2720,7 @@ } }, "inputMaxValue": { - "message": "Input value must not exceed $MAX$.", + "message": "Giá trị nhập vào không được vượt quá $MAX$.", "placeholders": { "max": { "content": "$1", @@ -2660,17 +2729,17 @@ } }, "multipleInputEmails": { - "message": "1 or more emails are invalid" + "message": "Có ít nhất 1 địa chỉ email không hợp lệ" }, "inputTrimValidator": { - "message": "Input must not contain only whitespace.", + "message": "Giá trị nhập vào không được chỉ có khoảng trắng.", "description": "Notification to inform the user that a form's input can't contain only whitespace." }, "inputEmail": { - "message": "Input is not an email address." + "message": "Giá trị nhập vào không phải là địa chỉ email." }, "fieldsNeedAttention": { - "message": "$COUNT$ field(s) above need your attention.", + "message": "Có $COUNT$ trường cần bạn xem xét ở trên.", "placeholders": { "count": { "content": "$1", @@ -2679,22 +2748,22 @@ } }, "selectPlaceholder": { - "message": "-- Select --" + "message": "-- Chọn --" }, "multiSelectPlaceholder": { - "message": "-- Type to filter --" + "message": "-- Nhập để lọc --" }, "multiSelectLoading": { - "message": "Retrieving options..." + "message": "Đang tải các tuỳ chọn..." }, "multiSelectNotFound": { - "message": "No items found" + "message": "Không tìm thấy mục nào" }, "multiSelectClearAll": { - "message": "Clear all" + "message": "Xoá tất cả" }, "plusNMore": { - "message": "+ $QUANTITY$ more", + "message": "+ $QUANTITY$ nhiều hơn", "placeholders": { "quantity": { "content": "$1", @@ -2703,47 +2772,47 @@ } }, "submenu": { - "message": "Submenu" + "message": "Menu con" }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "Ẩn/hiện thanh điều hướng bên" }, "skipToContent": { - "message": "Skip to content" + "message": "Chuyển đến nội dung" }, "typePasskey": { - "message": "Passkey" + "message": "Mã khoá" }, "passkeyNotCopied": { - "message": "Passkey will not be copied" + "message": "Không thể sao chép mã khoá" }, "passkeyNotCopiedAlert": { - "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" + "message": "Bản sao sẽ không bao gồm mã khoá. Bạn có muốn tiếp tục tạo bản sao mục này?" }, "aliasDomain": { - "message": "Alias domain" + "message": "Tên miền thay thế" }, "importData": { - "message": "Import data", + "message": "Nhập dữ liệu", "description": "Used for the desktop menu item and the header of the import dialog" }, "importError": { - "message": "Import error" + "message": "Lỗi nhập" }, "importErrorDesc": { - "message": "There was a problem with the data you tried to import. Please resolve the errors listed below in your source file and try again." + "message": "Có vấn đề với dữ liệu bạn cố gắng nhập. Vui lòng khắc phục các lỗi được liệt kê bên dưới trong tập tin nguồn của bạn và thử lại." }, "resolveTheErrorsBelowAndTryAgain": { - "message": "Resolve the errors below and try again." + "message": "Giải quyết các lỗi bên dưới và thử lại." }, "description": { - "message": "Description" + "message": "Mô tả" }, "importSuccess": { - "message": "Data successfully imported" + "message": "Dữ liệu đã được nhập thành công" }, "importSuccessNumberOfItems": { - "message": "A total of $AMOUNT$ items were imported.", + "message": "Đã nhập tổng cộng $AMOUNT$ mục.", "placeholders": { "amount": { "content": "$1", @@ -2752,10 +2821,10 @@ } }, "total": { - "message": "Total" + "message": "Tổng" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "Bạn đang nhập dữ liệu vào $ORGANIZATION$. Dữ liệu của bạn có thể được chia sẻ với các thành viên của tổ chức này. Bạn có muốn tiếp tục không?", "placeholders": { "organization": { "content": "$1", @@ -2763,41 +2832,44 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Lỗi kết nối với dịch vụ Duo. Sử dụng phương thức đăng nhập hai bước khác hoặc liên hệ với Duo để được hỗ trợ." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "Khởi chạy Duo và làm theo các bước để hoàn tất đăng nhập." }, "duoRequiredByOrgForAccount": { - "message": "Duo two-step login is required for your account." + "message": "Tài khoản của bạn yêu cầu xác minh hai bước với Duo." }, "launchDuo": { - "message": "Launch Duo in Browser" + "message": "Khởi chạy Duo trong trình duyệt" }, "importFormatError": { - "message": "Data is not formatted correctly. Please check your import file and try again." + "message": "Dữ liệu không được định dạng đúng. Vui lòng kiểm tra tập tin nhập và thử lại." }, "importNothingError": { - "message": "Nothing was imported." + "message": "Không có gì được nhập." }, "importEncKeyError": { - "message": "Error decrypting the exported file. Your encryption key does not match the encryption key used export the data." + "message": "Lỗi giải mã tập tin đã xuất. Khóa mã hóa của bạn không khớp với khóa mã hóa được sử dụng để xuất dữ liệu." }, "invalidFilePassword": { - "message": "Invalid file password, please use the password you entered when you created the export file." + "message": "Mật khẩu tập tin không hợp lệ, vui lòng sử dụng mật khẩu bạn đã nhập khi xuất tập tin." }, - "importDestination": { - "message": "Import destination" + "destination": { + "message": "Đến" }, "learnAboutImportOptions": { - "message": "Learn about your import options" + "message": "Tìm hiểu các tuỳ chọn nhập của bạn" }, "selectImportFolder": { - "message": "Select a folder" + "message": "Chọn thư mục" }, "selectImportCollection": { - "message": "Select a collection" + "message": "Chọn bộ sưu tập" }, "importTargetHint": { - "message": "Select this option if you want the imported file contents moved to a $DESTINATION$", + "message": "Chọn tùy chọn này để di chuyển nội dung tập tin đã được nhập đến $DESTINATION$", "description": "Located as a hint under the import target. Will be appended by either folder or collection, depending if the user is importing into an individual or an organizational vault.", "placeholders": { "destination": { @@ -2807,25 +2879,25 @@ } }, "importUnassignedItemsError": { - "message": "File contains unassigned items." + "message": "Tập tin chứa các mục không xác định." }, "selectFormat": { - "message": "Select the format of the import file" + "message": "Chọn định dạng tập tin nhập" }, "selectImportFile": { - "message": "Select the import file" + "message": "Chọn tập tin nhập" }, "chooseFile": { - "message": "Choose File" + "message": "Chọn tập tin" }, "noFileChosen": { - "message": "No file chosen" + "message": "Chưa chọn tập tin nào" }, "orCopyPasteFileContents": { - "message": "or copy/paste the import file contents" + "message": "hoặc sao chép/dán nội dung của tập tin nhập" }, "instructionsFor": { - "message": "$NAME$ Instructions", + "message": "Hướng dẫn dùng $NAME$", "description": "The title for the import tool instructions.", "placeholders": { "name": { @@ -2835,120 +2907,120 @@ } }, "confirmVaultImport": { - "message": "Confirm vault import" + "message": "Xác nhận nhập kho" }, "confirmVaultImportDesc": { - "message": "This file is password-protected. Please enter the file password to import data." + "message": "Tập tin này được bảo vệ bằng mật khẩu. Vui lòng nhập mật khẩu để nhập dữ liệu." }, "confirmFilePassword": { - "message": "Confirm file password" + "message": "Nhập lại mật khẩu tập tin" }, "exportSuccess": { - "message": "Vault data exported" + "message": "Đã xuất dữ liệu kho của bạn" }, "multifactorAuthenticationCancelled": { - "message": "Multifactor authentication cancelled" + "message": "Đã hủy xác thực đa yếu tố" }, "noLastPassDataFound": { - "message": "No LastPass data found" + "message": "Không tìm thấy dữ liệu LastPass" }, "incorrectUsernameOrPassword": { - "message": "Incorrect username or password" + "message": "Tên người dùng hoặc mật khẩu không đúng" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "Mật khẩu không đúng" }, "incorrectCode": { - "message": "Incorrect code" + "message": "Mã không đúng" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "Mã PIN không đúng" }, "multifactorAuthenticationFailed": { - "message": "Multifactor authentication failed" + "message": "Xác thực đa yếu tố thất bại" }, "includeSharedFolders": { - "message": "Include shared folders" + "message": "Bao gồm các thư mục được chia sẻ" }, "lastPassEmail": { - "message": "LastPass Email" + "message": "Email LastPass" }, "importingYourAccount": { - "message": "Importing your account..." + "message": "Đang nhập tài khoản của bạn..." }, "lastPassMFARequired": { - "message": "LastPass multifactor authentication required" + "message": "Yêu cầu xác thực đa yếu tố LastPass" }, "lastPassMFADesc": { - "message": "Enter your one-time passcode from your authentication app" + "message": "Nhập mã OTP từ ứng dụng xác thực của bạn" }, "lastPassOOBDesc": { - "message": "Approve the login request in your authentication app or enter a one-time passcode." + "message": "Phê duyệt yêu cầu đăng nhập trên ứng dụng xác thực của bạn hoặc nhập mã OTP." }, "passcode": { - "message": "Passcode" + "message": "Mật mã" }, "lastPassMasterPassword": { - "message": "LastPass master password" + "message": "Mật khẩu chính LastPass" }, "lastPassAuthRequired": { - "message": "LastPass authentication required" + "message": "Yêu cầu xác thực LastPass" }, "awaitingSSO": { - "message": "Awaiting SSO authentication" + "message": "Đang chờ xác thực SSO" }, "awaitingSSODesc": { - "message": "Please continue to log in using your company credentials." + "message": "Vui lòng tiếp tục đăng nhập bằng thông tin đăng nhập của công ty bạn." }, "seeDetailedInstructions": { - "message": "See detailed instructions on our help site at", + "message": "Xem hướng dẫn chi tiết trên trang trợ giúp của chúng tôi tại", "description": "This is followed a by a hyperlink to the help website." }, "importDirectlyFromLastPass": { - "message": "Import directly from LastPass" + "message": "Nhập trực tiếp từ LastPass" }, "importFromCSV": { - "message": "Import from CSV" + "message": "Nhập từ CSV" }, "lastPassTryAgainCheckEmail": { - "message": "Try again or look for an email from LastPass to verify it's you." + "message": "Thử lại hoặc tìm email từ LastPass để xác minh đó là bạn." }, "collection": { - "message": "Collection" + "message": "Bộ Sưu Tập" }, "lastPassYubikeyDesc": { - "message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button." + "message": "Cắm khóa YubiKey được liên kết với tài khoản LastPass của bạn vào cổng USB của máy tính, sau đó nhấn nút trên YubiKey." }, "commonImportFormats": { - "message": "Common formats", + "message": "Định dạng chung", "description": "Label indicating the most common import formats" }, "success": { - "message": "Success" + "message": "Thành công" }, "troubleshooting": { - "message": "Troubleshooting" + "message": "Khắc phục sự cố" }, "disableHardwareAccelerationRestart": { - "message": "Disable hardware acceleration and restart" + "message": "Vô hiệu hoá tính năng tăng tốc phần cứng và khởi động lại" }, "enableHardwareAccelerationRestart": { - "message": "Enable hardware acceleration and restart" + "message": "Kích hoạt tính năng tăng tốc phần cứng và khởi động lại" }, "removePasskey": { - "message": "Remove passkey" + "message": "Xóa mã khoá" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Đã xóa mã khoá" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "Lỗi khi gán vào bộ sưu tập chỉ định." }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "Lỗi khi gán vào thư mục chỉ định." }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "Xem các mục trong $NAME$", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -2958,7 +3030,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "Quay lại $NAME$", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -2968,11 +3040,11 @@ } }, "back": { - "message": "Back", + "message": "Quay lại", "description": "Button text to navigate back" }, "removeItem": { - "message": "Remove $NAME$", + "message": "Xoá $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Dữ liệu" + }, + "fileSends": { + "message": "Gửi file" + }, + "textSends": { + "message": "Gửi tin nhắn" + }, + "ssoError": { + "message": "Không thể tìm thấy cổng trống để đăng nhập SSO." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 0d25428e768..1c2fb3d5bee 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": "护照号码" @@ -494,7 +494,7 @@ "message": "文件夹已删除" }, "loginOrCreateNewAccount": { - "message": "登录或者创建一个账户来访问您的安全密码库。" + "message": "登录或创建一个新账户以访问您的安全密码库。" }, "createAccount": { "message": "创建账户" @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "主密码" + }, + "masterPassImportant": { + "message": "主密码忘记后,将无法恢复!" + }, + "confirmMasterPassword": { + "message": "确认主密码" + }, + "masterPassHintLabel": { + "message": "主密码提示" + }, + "joinOrganization": { + "message": "加入组织" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "通过设置主密码完成加入此组织。" + }, "settings": { "message": "设置" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "您的新账户已创建!您现在可以登录了。" }, + "newAccountCreated2": { + "message": "您的新账户已成功创建!" + }, + "youHaveBeenLoggedIn": { + "message": "您已登录!" + }, "masterPassSent": { "message": "我们已经为您发送了包含主密码提示的邮件。" }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "必须填写验证码。" }, + "webauthnCancelOrTimeout": { + "message": "身份验证被取消或耗时过长。请重试。" + }, "invalidVerificationCode": { "message": "无效的验证码" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "您的登录会话已过期。" }, + "restartRegistration": { + "message": "重新开始注册" + }, + "expiredLink": { + "message": "失效链接" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "请重新注册或尝试登录。" + }, + "youMayAlreadyHaveAnAccount": { + "message": "您可能已经有一个账户了" + }, "logOutConfirmation": { "message": "确定要注销吗?" }, @@ -799,7 +838,7 @@ "message": "账户" }, "loading": { - "message": "正在加载..." + "message": "加载中…" }, "lockVault": { "message": "锁定密码库" @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "您可以在 bitwarden.com 网页版密码库购买高级会员。现在要访问吗?" }, + "premiumPurchaseAlertV2": { + "message": "您可以在 Bitwarden 网页 App 的账户设置中购买高级版。" + }, "premiumCurrentMember": { "message": "您目前是高级会员!" }, @@ -1148,7 +1190,7 @@ "message": "感谢您支持 Bitwarden。" }, "premiumPrice": { - "message": "只需 $PRICE$ /年!", + "message": "全部仅需 $PRICE$ /年!", "placeholders": { "price": { "content": "$1", @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "访问令牌刷新错误" }, @@ -1477,9 +1522,15 @@ "additionalWindowsHelloSettings": { "message": "额外的 Windows Hello 设置" }, + "unlockWithPolkit": { + "message": "使用系统身份验证解锁" + }, "windowsHelloConsentMessage": { "message": "验证 Bitwarden。" }, + "polkitConsentMessage": { + "message": "验证以解锁 Bitwarden。" + }, "unlockWithTouchId": { "message": "使用触控 ID 解锁" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "应用程序启动时要求使用 Windows Hello" }, + "autoPromptPolkit": { + "message": "启动时请求系统身份验证" + }, "autoPromptTouchId": { "message": "应用程序启动时要求使用触控 ID" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "您的新主密码不符合策略要求。" }, - "receiveMarketingEmails": { - "message": "接收来自 Bitwarden 的电子邮件,以获取公告、建议和调研。" + "receiveMarketingEmailsV2": { + "message": "获取来自 Bitwarden 的建议、公告和调研电子邮件。" }, "unsubscribe": { "message": "取消订阅" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "需要先在桌面应用程序的设置中设置生物识别,才能使用浏览器中的生物识别。" }, + "biometricsManualSetupTitle": { + "message": "自动设置不可用" + }, + "biometricsManualSetupDesc": { + "message": "由于安装方式的原因,生物识别支持无法自动启用。您想要打开关于如何手动执行此操作的文档吗?" + }, "personalOwnershipSubmitError": { "message": "由于某个企业策略,您不能将项目保存到您的个人密码库。将所有权选项更改为组织,并从可用的集合中选择。" }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "需要验证电子邮件" }, + "emailVerifiedV2": { + "message": "电子邮箱已验证" + }, "emailVerificationRequiredDesc": { "message": "您必须验证您的电子邮件才能使用此功能。" }, @@ -1979,8 +2042,11 @@ "updateWeakMasterPasswordWarning": { "message": "您的主密码不符合某一项或多项组织策略要求。要访问密码库,必须立即更新您的主密码。继续操作将使您退出当前会话,并要求您重新登录。其他设备上的活动会话可能会继续保持活动状态长达一小时。" }, + "tdeDisabledMasterPasswordRequired": { + "message": "您的组织禁用了信任设备加密。要访问您的密码库,请设置一个主密码。" + }, "tryAgain": { - "message": "再试一次" + "message": "请重试" }, "verificationRequiredForActionSetPinToContinue": { "message": "此操作需要验证。设置一个 PIN 码以继续。" @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "您的密码库超时时间超出了组织设置的限制。" }, + "inviteAccepted": { + "message": "邀请已接受" + }, "resetPasswordPolicyAutoEnroll": { "message": "自动注册" }, @@ -2396,7 +2465,7 @@ "message": "指纹短语" }, "needAnotherOption": { - "message": "设备登录必须在 Bitwarden 应用程序的设置中启用。需要其他登录选项吗?" + "message": "必须在 Bitwarden App 的设置中启用设备登录。需要其他登录选项吗?" }, "viewAllLoginOptions": { "message": "查看所有登录选项" @@ -2482,7 +2551,7 @@ "message": "已请求登录" }, "creatingAccountOn": { - "message": "创建账户于" + "message": "正创建账户于" }, "checkYourEmail": { "message": "检查您的电子邮箱" @@ -2688,7 +2757,7 @@ "message": "正在获取选项..." }, "multiSelectNotFound": { - "message": "未找到任何条目" + "message": "未找到任何项目" }, "multiSelectClearAll": { "message": "清除全部" @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "与 Duo 服务连接时出错。使用不同的两步登录方式或联系 Duo 寻求帮助。" + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "启动 Duo 然后按照步骤完成登录。" }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "无效的文件密码,请使用您创建导出文件时输入的密码。" }, - "importDestination": { - "message": "导入目的地" + "destination": { + "message": "目的地" }, "learnAboutImportOptions": { "message": "了解您的导入选项" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "数据" + }, + "fileSends": { + "message": "文件 Send" + }, + "textSends": { + "message": "文本 Send" + }, + "ssoError": { + "message": "找不到用于 SSO 登录的可用端口。" + }, + "fileSavedToDevice": { + "message": "文件已保存到设备。可以在设备下载中进行管理。" } } diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index 75ac7dfb3fe..984986bf94c 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -539,6 +539,24 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, + "joinOrganization": { + "message": "Join organization" + }, + "finishJoiningThisOrganizationBySettingAMasterPassword": { + "message": "Finish joining this organization by setting a master password." + }, "settings": { "message": "設定" }, @@ -585,6 +603,12 @@ "newAccountCreated": { "message": "帳戶已建立!現在可以登入了。" }, + "newAccountCreated2": { + "message": "Your new account has been created!" + }, + "youHaveBeenLoggedIn": { + "message": "You have been logged in!" + }, "masterPassSent": { "message": "已寄出包含您主密碼提示的電子郵件。" }, @@ -615,6 +639,9 @@ "verificationCodeRequired": { "message": "必須填入驗證碼。" }, + "webauthnCancelOrTimeout": { + "message": "The authentication was cancelled or took too long. Please try again." + }, "invalidVerificationCode": { "message": "無效的驗證碼" }, @@ -777,6 +804,18 @@ "loginExpired": { "message": "您的登入會話已過期。" }, + "restartRegistration": { + "message": "Restart registration" + }, + "expiredLink": { + "message": "Expired link" + }, + "pleaseRestartRegistrationOrTryLoggingIn": { + "message": "Please restart registration or try logging in." + }, + "youMayAlreadyHaveAnAccount": { + "message": "You may already have an account" + }, "logOutConfirmation": { "message": "您確定要登出嗎?" }, @@ -1141,6 +1180,9 @@ "premiumPurchaseAlert": { "message": "您可以在 bitwarden.com 網頁版密碼庫購買進階會員資格。現在要前往嗎?" }, + "premiumPurchaseAlertV2": { + "message": "You can purchase Premium from your account settings on the Bitwarden web app." + }, "premiumCurrentMember": { "message": "您目前是進階會員!" }, @@ -1243,6 +1285,9 @@ } } }, + "copySuccessful": { + "message": "Copy Successful" + }, "errorRefreshingAccessToken": { "message": "Access Token Refresh Error" }, @@ -1349,7 +1394,7 @@ "message": "This file export will be password protected and require the file password to decrypt." }, "filePassword": { - "message": "File password" + "message": "檔案密碼" }, "exportPasswordDescription": { "message": "This password will be used to export and import this file" @@ -1477,9 +1522,15 @@ "additionalWindowsHelloSettings": { "message": "額外的 Windows Hello 設定" }, + "unlockWithPolkit": { + "message": "Unlock with system authentication" + }, "windowsHelloConsentMessage": { "message": "驗證 Bitwarden。" }, + "polkitConsentMessage": { + "message": "Authenticate to unlock Bitwarden." + }, "unlockWithTouchId": { "message": "使用 Touch ID 解鎖" }, @@ -1492,6 +1543,9 @@ "autoPromptWindowsHello": { "message": "啟動時詢問 Windows Hello" }, + "autoPromptPolkit": { + "message": "Ask for system authentication on launch" + }, "autoPromptTouchId": { "message": "啟動時詢問 Touch ID" }, @@ -1678,8 +1732,8 @@ "masterPasswordPolicyRequirementsNotMet": { "message": "新的主密碼不符合原則要求。" }, - "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "receiveMarketingEmailsV2": { + "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." }, "unsubscribe": { "message": "Unsubscribe" @@ -1771,6 +1825,12 @@ "biometricsNotEnabledDesc": { "message": "需先在桌面應用程式的設定中啟用生物特徵辨識,才能使用瀏覽器的生物特徵辨識。" }, + "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": "由於某個企業原則,您被限制為儲存項目到您的個人密碼庫。將擁有權變更為組織,並從可用的集合中選擇。" }, @@ -1955,6 +2015,9 @@ "emailVerificationRequired": { "message": "需要驗證電子郵件" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "必須驗證您的電子郵件才能使用此功能。" }, @@ -1979,6 +2042,9 @@ "updateWeakMasterPasswordWarning": { "message": "您的主密碼不符合一個或多個組織原則要求。您必須立即更新您的主密碼才能存取密碼庫。進行此動作將登出您目前的工作階段,需要您重新登入。其他裝置上的工作階段可能繼續長達一小時。" }, + "tdeDisabledMasterPasswordRequired": { + "message": "Your organization has disabled trusted device encryption. Please set a master password to access your vault." + }, "tryAgain": { "message": "再試一次" }, @@ -2063,6 +2129,9 @@ "vaultTimeoutTooLarge": { "message": "您的密碼庫逾時時間超過組織設定的限制。" }, + "inviteAccepted": { + "message": "Invitation accepted" + }, "resetPasswordPolicyAutoEnroll": { "message": "自動註冊" }, @@ -2763,6 +2832,9 @@ } } }, + "duoHealthCheckResultsInNullAuthUrlError": { + "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "Launch Duo and follow the steps to finish logging in." }, @@ -2784,8 +2856,8 @@ "invalidFilePassword": { "message": "檔案密碼無效,請使用您當初匯出檔案時輸入的密碼。" }, - "importDestination": { - "message": "匯入目的地" + "destination": { + "message": "Destination" }, "learnAboutImportOptions": { "message": "瞭解更多匯入選項" @@ -2980,5 +3052,20 @@ "example": "Work" } } + }, + "data": { + "message": "Data" + }, + "fileSends": { + "message": "File Sends" + }, + "textSends": { + "message": "Text Sends" + }, + "ssoError": { + "message": "No free ports could be found for the sso login." + }, + "fileSavedToDevice": { + "message": "File saved to device. Manage from your device downloads." } } diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index f2295f2cdd8..86d07440a73 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -32,14 +32,16 @@ import { PowerMonitorMain } from "./main/power-monitor.main"; import { TrayMain } from "./main/tray.main"; import { UpdaterMain } from "./main/updater.main"; import { WindowMain } from "./main/window.main"; -import { BiometricsService, BiometricsServiceAbstraction } from "./platform/main/biometric/index"; +import { BiometricsService, DesktopBiometricsService } from "./platform/main/biometric/index"; import { ClipboardMain } from "./platform/main/clipboard.main"; import { DesktopCredentialStorageListener } from "./platform/main/desktop-credential-storage-listener"; import { MainCryptoFunctionService } from "./platform/main/main-crypto-function.service"; import { DesktopSettingsService } from "./platform/services/desktop-settings.service"; import { ElectronLogMainService } from "./platform/services/electron-log.main.service"; import { ElectronStorageService } from "./platform/services/electron-storage.service"; +import { EphemeralValueStorageService } from "./platform/services/ephemeral-value-storage.main.service"; import { I18nMainService } from "./platform/services/i18n.main.service"; +import { SSOLocalhostCallbackService } from "./platform/services/sso-localhost-callback.service"; import { ElectronMainMessagingService } from "./services/electron-main-messaging.service"; import { isMacAppStore } from "./utils"; @@ -62,7 +64,7 @@ export class Main { menuMain: MenuMain; powerMonitorMain: PowerMonitorMain; trayMain: TrayMain; - biometricsService: BiometricsServiceAbstraction; + biometricsService: DesktopBiometricsService; nativeMessagingMain: NativeMessagingMain; clipboardMain: ClipboardMain; desktopAutofillSettingsService: DesktopAutofillSettingsService; @@ -109,7 +111,10 @@ export class Main { this.storageService, this.memoryStorageForStateProviders, ); - const globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider); + const globalStateProvider = new DefaultGlobalStateProvider( + storageServiceProvider, + this.logService, + ); this.i18nService = new I18nMainService("en", "./locales/", globalStateProvider); @@ -130,6 +135,7 @@ export class Main { const singleUserStateProvider = new DefaultSingleUserStateProvider( storageServiceProvider, stateEventRegistrarService, + this.logService, ); const activeUserStateProvider = new DefaultActiveUserStateProvider( @@ -184,7 +190,7 @@ export class Main { }); }); - this.powerMonitorMain = new PowerMonitorMain(this.messagingService); + this.powerMonitorMain = new PowerMonitorMain(this.messagingService, this.logService); this.menuMain = new MenuMain( this.i18nService, this.messagingService, @@ -220,6 +226,9 @@ export class Main { this.clipboardMain = new ClipboardMain(); this.clipboardMain.init(); + + new EphemeralValueStorageService(); + new SSOLocalhostCallbackService(this.environmentService, this.messagingService); } bootstrap() { diff --git a/apps/desktop/src/main/power-monitor.main.ts b/apps/desktop/src/main/power-monitor.main.ts index 8cad5c1d9e2..b51c4959b8f 100644 --- a/apps/desktop/src/main/power-monitor.main.ts +++ b/apps/desktop/src/main/power-monitor.main.ts @@ -1,6 +1,8 @@ -import { powerMonitor } from "electron"; +import { ipcMain, powerMonitor } from "electron"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessageSender } from "@bitwarden/common/platform/messaging"; +import { powermonitors } from "@bitwarden/desktop-napi"; import { isSnapStore } from "../utils"; @@ -11,7 +13,10 @@ const IdleCheckInterval = 30 * 1000; // 30 seconds export class PowerMonitorMain { private idle = false; - constructor(private messagingService: MessageSender) {} + constructor( + private messagingService: MessageSender, + private logService: LogService, + ) {} init() { // ref: https://github.com/electron/electron/issues/13767 @@ -27,7 +32,22 @@ export class PowerMonitorMain { powerMonitor.on("lock-screen", () => { this.messagingService.send("systemLocked"); }); + } else { + powermonitors + .onLock(() => { + this.messagingService.send("systemLocked"); + }) + .catch((error) => { + this.logService.error("Error setting up lock monitor", { error }); + }); } + ipcMain.handle("powermonitor.isLockMonitorAvailable", async (_event: any, _message: any) => { + if (process.platform !== "linux") { + return true; + } else { + return await powermonitors.isLockMonitorAvailable(); + } + }); // System idle global.setInterval(() => { diff --git a/apps/desktop/src/main/window.main.ts b/apps/desktop/src/main/window.main.ts index e82d16ee9fd..029a0527c6e 100644 --- a/apps/desktop/src/main/window.main.ts +++ b/apps/desktop/src/main/window.main.ts @@ -8,6 +8,7 @@ import { firstValueFrom } from "rxjs"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { processisolations } from "@bitwarden/desktop-napi"; import { WindowState } from "../platform/models/domain/window-state"; import { DesktopSettingsService } from "../platform/services/desktop-settings.service"; @@ -31,6 +32,7 @@ export class WindowMain { private windowStateChangeTimer: NodeJS.Timeout; private windowStates: { [key: string]: WindowState } = {}; private enableAlwaysOnTop = false; + private enableRendererProcessForceCrashReload = false; session: Electron.Session; readonly defaultWidth = 950; @@ -49,11 +51,12 @@ export class WindowMain { // Perform a hard reload of the render process by crashing it. This is suboptimal but ensures that all memory gets // cleared, as the process itself will be completely garbage collected. ipcMain.on("reload-process", async () => { + this.logService.info("Reloading render process"); // User might have changed theme, ensure the window is updated. this.win.setBackgroundColor(await this.getBackgroundColor()); // By default some linux distro collect core dumps on crashes which gets written to disk. - if (!isLinux()) { + if (this.enableRendererProcessForceCrashReload) { const crashEvent = once(this.win.webContents, "render-process-gone"); this.win.webContents.forcefullyCrashRenderer(); await crashEvent; @@ -63,6 +66,7 @@ export class WindowMain { // 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.session.clearCache(); + this.logService.info("Render process reloaded"); }); return new Promise((resolve, reject) => { @@ -103,6 +107,33 @@ export class WindowMain { // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.on("ready", async () => { + if (isMac() || isWindows()) { + this.enableRendererProcessForceCrashReload = true; + } else if (isLinux() && !isDev()) { + if (await processisolations.isCoreDumpingDisabled()) { + this.logService.info("Coredumps are disabled in renderer process"); + this.enableRendererProcessForceCrashReload = true; + } else { + this.logService.info("Disabling coredumps in main process"); + try { + await processisolations.disableCoredumps(); + } catch (e) { + this.logService.error("Failed to disable coredumps", e); + } + } + + // this currently breaks the file portal, so should only be used when + // no files are needed but security requirements are super high https://github.com/flatpak/xdg-desktop-portal/issues/785 + if (process.env.EXPERIMENTAL_PREVENT_DEBUGGER_MEMORY_ACCESS === "true") { + this.logService.info("Disabling memory dumps in main process"); + try { + await processisolations.disableMemoryAccess(); + } catch (e) { + this.logService.error("Failed to disable memory dumps", e); + } + } + } + await this.createWindow(); resolve(); if (this.argvCallback != null) { diff --git a/apps/desktop/src/models/account.ts b/apps/desktop/src/models/account.ts deleted file mode 100644 index b3d31284139..00000000000 --- a/apps/desktop/src/models/account.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { - Account as BaseAccount, - AccountSettings as BaseAccountSettings, -} from "@bitwarden/common/platform/models/domain/account"; - -export class AccountSettings extends BaseAccountSettings { - dismissedBiometricRequirePasswordOnStartCallout?: boolean; -} - -export class Account extends BaseAccount { - settings?: AccountSettings = new AccountSettings(); - - constructor(init: Partial) { - super(init); - Object.assign(this.settings, { - ...new AccountSettings(), - ...this.settings, - }); - } -} diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 4ba7c6b6336..6823bddceb8 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,20 +1,20 @@ { "name": "@bitwarden/desktop", - "version": "2024.7.1", + "version": "2024.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.7.1", + "version": "2024.9.0", "license": "GPL-3.0", "dependencies": { - "@bitwarden/desktop-napi": "file:../desktop_native", + "@bitwarden/desktop-napi": "file:../desktop_native/napi", "argon2": "0.40.1" } }, - "../desktop_native": { - "name": "@bitwarden/desktop-native", + "../desktop_native/napi": { + "name": "@bitwarden/desktop-napi", "version": "0.1.0", "license": "GPL-3.0", "devDependencies": { @@ -22,13 +22,14 @@ } }, "node_modules/@bitwarden/desktop-napi": { - "resolved": "../desktop_native", + "resolved": "../desktop_native/napi", "link": true }, "node_modules/@phc/format": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==", + "license": "MIT", "engines": { "node": ">=10" } @@ -38,6 +39,7 @@ "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.40.1.tgz", "integrity": "sha512-DjtHDwd7pm12qeWyfihHoM8Bn5vGcgH6sKwgPqwNYroRmxlrzadHEvMyuvQxN/V8YSyRRKD5x6ito09q1e9OyA==", "hasInstallScript": true, + "license": "MIT", "dependencies": { "@phc/format": "^1.0.0", "node-addon-api": "^7.1.0", @@ -48,17 +50,16 @@ } }, "node_modules/node-addon-api": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.0.tgz", - "integrity": "sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==", - "engines": { - "node": "^16 || ^18 || >= 20" - } + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" }, "node_modules/node-gyp-build": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.0.tgz", - "integrity": "sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==", + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz", + "integrity": "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==", + "license": "MIT", "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 1793642dab6..18a046d5bce 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.1", + "version": "2024.9.0", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/desktop/src/platform/main/biometric/biometric.darwin.main.ts b/apps/desktop/src/platform/main/biometric/biometric.darwin.main.ts index e1a5c3da9a9..0f26cc78fbf 100644 --- a/apps/desktop/src/platform/main/biometric/biometric.darwin.main.ts +++ b/apps/desktop/src/platform/main/biometric/biometric.darwin.main.ts @@ -3,7 +3,7 @@ import { systemPreferences } from "electron"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { passwords } from "@bitwarden/desktop-napi"; -import { OsBiometricService } from "./biometrics.service.abstraction"; +import { OsBiometricService } from "./desktop.biometrics.service"; export default class BiometricDarwinMain implements OsBiometricService { constructor(private i18nservice: I18nService) {} @@ -51,4 +51,14 @@ export default class BiometricDarwinMain implements OsBiometricService { return false; } } + + async osBiometricsNeedsSetup() { + return false; + } + + async osBiometricsCanAutoSetup(): Promise { + return false; + } + + async osBiometricsSetup(): Promise {} } diff --git a/apps/desktop/src/platform/main/biometric/biometric.noop.main.ts b/apps/desktop/src/platform/main/biometric/biometric.noop.main.ts index 152b5ce32f7..57a86942e8c 100644 --- a/apps/desktop/src/platform/main/biometric/biometric.noop.main.ts +++ b/apps/desktop/src/platform/main/biometric/biometric.noop.main.ts @@ -1,4 +1,4 @@ -import { OsBiometricService } from "./biometrics.service.abstraction"; +import { OsBiometricService } from "./desktop.biometrics.service"; export default class NoopBiometricsService implements OsBiometricService { constructor() {} @@ -9,6 +9,16 @@ export default class NoopBiometricsService implements OsBiometricService { return false; } + async osBiometricsNeedsSetup(): Promise { + return false; + } + + async osBiometricsCanAutoSetup(): Promise { + return false; + } + + async osBiometricsSetup(): Promise {} + async getBiometricKey( service: string, storageKey: string, diff --git a/apps/desktop/src/platform/main/biometric/biometric.unix.main.ts b/apps/desktop/src/platform/main/biometric/biometric.unix.main.ts new file mode 100644 index 00000000000..c748276a6ef --- /dev/null +++ b/apps/desktop/src/platform/main/biometric/biometric.unix.main.ts @@ -0,0 +1,160 @@ +import { spawn } from "child_process"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { biometrics, passwords } from "@bitwarden/desktop-napi"; + +import { WindowMain } from "../../../main/window.main"; +import { isFlatpak, isLinux, isSnapStore } from "../../../utils"; + +import { OsBiometricService } from "./desktop.biometrics.service"; + +const polkitPolicy = ` + + + + + Unlock Bitwarden + Authenticate to unlock Bitwarden + + no + no + auth_self + + +`; +const policyFileName = "com.bitwarden.Bitwarden.policy"; +const policyPath = "/usr/share/polkit-1/actions/"; + +export default class BiometricUnixMain implements OsBiometricService { + constructor( + private i18nservice: I18nService, + private windowMain: WindowMain, + ) {} + private _iv: string | null = null; + // Use getKeyMaterial helper instead of direct access + private _osKeyHalf: string | null = null; + + async setBiometricKey( + service: string, + key: string, + value: string, + clientKeyPartB64: string | undefined, + ): Promise { + const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 }); + await biometrics.setBiometricSecret( + service, + key, + value, + storageDetails.key_material, + storageDetails.ivB64, + ); + } + async deleteBiometricKey(service: string, key: string): Promise { + await passwords.deletePassword(service, key); + } + + async getBiometricKey( + service: string, + storageKey: string, + clientKeyPartB64: string | undefined, + ): Promise { + const success = await this.authenticateBiometric(); + + if (!success) { + throw new Error("Biometric authentication failed"); + } + + const value = await passwords.getPassword(service, storageKey); + + if (value == null || value == "") { + return null; + } else { + const encValue = new EncString(value); + this.setIv(encValue.iv); + const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 }); + const storedValue = await biometrics.getBiometricSecret( + service, + storageKey, + storageDetails.key_material, + ); + return storedValue; + } + } + + async authenticateBiometric(): Promise { + const hwnd = this.windowMain.win.getNativeWindowHandle(); + return await biometrics.prompt(hwnd, this.i18nservice.t("polkitConsentMessage")); + } + + async osSupportsBiometric(): Promise { + // We assume all linux distros have some polkit implementation + // that either has bitwarden set up or not, which is reflected in osBiomtricsNeedsSetup. + // Snap does not have access at the moment to polkit + // This could be dynamically detected on dbus in the future. + // We should check if a libsecret implementation is available on the system + // because otherwise we cannot offlod the protected userkey to secure storage. + return (await passwords.isAvailable()) && !isSnapStore(); + } + + async osBiometricsNeedsSetup(): Promise { + // check whether the polkit policy is loaded via dbus call to polkit + return !(await biometrics.available()); + } + + async osBiometricsCanAutoSetup(): Promise { + // We cannot auto setup on snap or flatpak since the filesystem is sandboxed. + // The user needs to manually set up the polkit policy outside of the sandbox + // since we allow access to polkit via dbus for the sandboxed clients, the authentication works from + // the sandbox, once the policy is set up outside of the sandbox. + return isLinux() && !isSnapStore() && !isFlatpak(); + } + + async osBiometricsSetup(): Promise { + const process = spawn("pkexec", [ + "bash", + "-c", + `echo '${polkitPolicy}' > ${policyPath + policyFileName} && chown root:root ${policyPath + policyFileName} && chcon system_u:object_r:usr_t:s0 ${policyPath + policyFileName}`, + ]); + + await new Promise((resolve, reject) => { + process.on("close", (code) => { + if (code !== 0) { + reject("Failed to set up polkit policy"); + } else { + resolve(null); + } + }); + }); + } + + // Nulls out key material in order to force a re-derive. This should only be used in getBiometricKey + // when we want to force a re-derive of the key material. + private setIv(iv: string) { + this._iv = iv; + this._osKeyHalf = null; + } + + private async getStorageDetails({ + clientKeyHalfB64, + }: { + clientKeyHalfB64: string; + }): Promise<{ key_material: biometrics.KeyMaterial; ivB64: string }> { + if (this._osKeyHalf == null) { + const keyMaterial = await biometrics.deriveKeyMaterial(this._iv); + // osKeyHalf is based on the iv and in contrast to windows is not locked behind user verefication! + this._osKeyHalf = keyMaterial.keyB64; + this._iv = keyMaterial.ivB64; + } + + return { + key_material: { + osKeyPartB64: this._osKeyHalf, + clientKeyPartB64: clientKeyHalfB64, + }, + ivB64: this._iv, + }; + } +} diff --git a/apps/desktop/src/platform/main/biometric/biometric.windows.main.ts b/apps/desktop/src/platform/main/biometric/biometric.windows.main.ts index 75d5bce8f50..95f433c39e0 100644 --- a/apps/desktop/src/platform/main/biometric/biometric.windows.main.ts +++ b/apps/desktop/src/platform/main/biometric/biometric.windows.main.ts @@ -6,7 +6,7 @@ import { biometrics, passwords } from "@bitwarden/desktop-napi"; import { WindowMain } from "../../../main/window.main"; -import { OsBiometricService } from "./biometrics.service.abstraction"; +import { OsBiometricService } from "./desktop.biometrics.service"; const KEY_WITNESS_SUFFIX = "_witness"; const WITNESS_VALUE = "known key"; @@ -214,4 +214,14 @@ export default class BiometricWindowsMain implements OsBiometricService { clientKeyPartB64, }; } + + async osBiometricsNeedsSetup() { + return false; + } + + async osBiometricsCanAutoSetup(): Promise { + return false; + } + + async osBiometricsSetup(): Promise {} } diff --git a/apps/desktop/src/platform/main/biometric/biometrics.service.abstraction.ts b/apps/desktop/src/platform/main/biometric/biometrics.service.abstraction.ts deleted file mode 100644 index fb7ce048b5a..00000000000 --- a/apps/desktop/src/platform/main/biometric/biometrics.service.abstraction.ts +++ /dev/null @@ -1,42 +0,0 @@ -export abstract class BiometricsServiceAbstraction { - abstract osSupportsBiometric(): Promise; - abstract canAuthBiometric({ - service, - key, - userId, - }: { - service: string; - key: string; - userId: string; - }): Promise; - abstract authenticateBiometric(): Promise; - abstract getBiometricKey(service: string, key: string): Promise; - abstract setBiometricKey(service: string, key: string, value: string): Promise; - abstract setEncryptionKeyHalf({ - service, - key, - value, - }: { - service: string; - key: string; - value: string; - }): void; - abstract deleteBiometricKey(service: string, key: string): Promise; -} - -export interface OsBiometricService { - osSupportsBiometric(): Promise; - authenticateBiometric(): Promise; - getBiometricKey( - service: string, - key: string, - clientKeyHalfB64: string | undefined, - ): Promise; - setBiometricKey( - service: string, - key: string, - value: string, - clientKeyHalfB64: string | undefined, - ): Promise; - deleteBiometricKey(service: string, key: string): Promise; -} diff --git a/apps/desktop/src/platform/main/biometric/biometrics.service.spec.ts b/apps/desktop/src/platform/main/biometric/biometrics.service.spec.ts index cb6bb4858c0..10ba1c83b64 100644 --- a/apps/desktop/src/platform/main/biometric/biometrics.service.spec.ts +++ b/apps/desktop/src/platform/main/biometric/biometrics.service.spec.ts @@ -11,7 +11,7 @@ import { WindowMain } from "../../../main/window.main"; import BiometricDarwinMain from "./biometric.darwin.main"; import BiometricWindowsMain from "./biometric.windows.main"; import { BiometricsService } from "./biometrics.service"; -import { OsBiometricService } from "./biometrics.service.abstraction"; +import { OsBiometricService } from "./desktop.biometrics.service"; jest.mock("@bitwarden/desktop-napi", () => { return { diff --git a/apps/desktop/src/platform/main/biometric/biometrics.service.ts b/apps/desktop/src/platform/main/biometric/biometrics.service.ts index b0331cce3e1..e432939c877 100644 --- a/apps/desktop/src/platform/main/biometric/biometrics.service.ts +++ b/apps/desktop/src/platform/main/biometric/biometrics.service.ts @@ -6,9 +6,9 @@ import { UserId } from "@bitwarden/common/types/guid"; import { WindowMain } from "../../../main/window.main"; -import { BiometricsServiceAbstraction, OsBiometricService } from "./biometrics.service.abstraction"; +import { DesktopBiometricsService, OsBiometricService } from "./desktop.biometrics.service"; -export class BiometricsService implements BiometricsServiceAbstraction { +export class BiometricsService extends DesktopBiometricsService { private platformSpecificService: OsBiometricService; private clientKeyHalves = new Map(); @@ -20,6 +20,7 @@ export class BiometricsService implements BiometricsServiceAbstraction { private platform: NodeJS.Platform, private biometricStateService: BiometricStateService, ) { + super(); this.loadPlatformSpecificService(this.platform); } @@ -28,6 +29,8 @@ export class BiometricsService implements BiometricsServiceAbstraction { this.loadWindowsHelloService(); } else if (platform === "darwin") { this.loadMacOSService(); + } else if (platform === "linux") { + this.loadUnixService(); } else { this.loadNoopBiometricsService(); } @@ -49,16 +52,34 @@ export class BiometricsService implements BiometricsServiceAbstraction { this.platformSpecificService = new BiometricDarwinMain(this.i18nService); } + private loadUnixService() { + // eslint-disable-next-line + const BiometricUnixMain = require("./biometric.unix.main").default; + this.platformSpecificService = new BiometricUnixMain(this.i18nService, this.windowMain); + } + private loadNoopBiometricsService() { // eslint-disable-next-line const NoopBiometricsService = require("./biometric.noop.main").default; this.platformSpecificService = new NoopBiometricsService(); } - async osSupportsBiometric() { + async supportsBiometric() { return await this.platformSpecificService.osSupportsBiometric(); } + async biometricsNeedsSetup() { + return await this.platformSpecificService.osBiometricsNeedsSetup(); + } + + async biometricsSupportsAutoSetup() { + return await this.platformSpecificService.osBiometricsCanAutoSetup(); + } + + async biometricsSetup() { + await this.platformSpecificService.osBiometricsSetup(); + } + async canAuthBiometric({ service, key, @@ -71,7 +92,7 @@ export class BiometricsService implements BiometricsServiceAbstraction { const requireClientKeyHalf = await this.biometricStateService.getRequirePasswordOnStart(userId); const clientKeyHalfB64 = this.getClientKeyHalf(service, key); const clientKeyHalfSatisfied = !requireClientKeyHalf || !!clientKeyHalfB64; - return clientKeyHalfSatisfied && (await this.osSupportsBiometric()); + return clientKeyHalfSatisfied && (await this.supportsBiometric()); } async authenticateBiometric(): Promise { @@ -90,6 +111,10 @@ export class BiometricsService implements BiometricsServiceAbstraction { return result; } + async isBiometricUnlockAvailable(): Promise { + return await this.platformSpecificService.osSupportsBiometric(); + } + async getBiometricKey(service: string, storageKey: string): Promise { return await this.interruptProcessReload(async () => { await this.enforceClientKeyHalf(service, storageKey); diff --git a/apps/desktop/src/platform/main/biometric/desktop.biometrics.service.ts b/apps/desktop/src/platform/main/biometric/desktop.biometrics.service.ts new file mode 100644 index 00000000000..c8e3a59612a --- /dev/null +++ b/apps/desktop/src/platform/main/biometric/desktop.biometrics.service.ts @@ -0,0 +1,62 @@ +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; + +/** + * This service extends the base biometrics service to provide desktop specific functions, + * specifically for the main process. + */ +export abstract class DesktopBiometricsService extends BiometricsService { + abstract canAuthBiometric({ + service, + key, + userId, + }: { + service: string; + key: string; + userId: string; + }): Promise; + abstract getBiometricKey(service: string, key: string): Promise; + abstract setBiometricKey(service: string, key: string, value: string): Promise; + abstract setEncryptionKeyHalf({ + service, + key, + value, + }: { + service: string; + key: string; + value: string; + }): void; + abstract deleteBiometricKey(service: string, key: string): Promise; +} + +export interface OsBiometricService { + osSupportsBiometric(): Promise; + /** + * Check whether support for biometric unlock requires setup. This can be automatic or manual. + * + * @returns true if biometrics support requires setup, false if it does not (is already setup, or did not require it in the first place) + */ + osBiometricsNeedsSetup: () => Promise; + /** + * Check whether biometrics can be automatically setup, or requires user interaction. + * + * @returns true if biometrics support can be automatically setup, false if it requires user interaction. + */ + osBiometricsCanAutoSetup: () => Promise; + /** + * Starts automatic biometric setup, which places the required configuration files / changes the required settings. + */ + osBiometricsSetup: () => Promise; + authenticateBiometric(): Promise; + getBiometricKey( + service: string, + key: string, + clientKeyHalfB64: string | undefined, + ): Promise; + setBiometricKey( + service: string, + key: string, + value: string, + clientKeyHalfB64: string | undefined, + ): Promise; + deleteBiometricKey(service: string, key: string): Promise; +} diff --git a/apps/desktop/src/platform/main/biometric/index.ts b/apps/desktop/src/platform/main/biometric/index.ts index f5a594d966f..ad7725d718a 100644 --- a/apps/desktop/src/platform/main/biometric/index.ts +++ b/apps/desktop/src/platform/main/biometric/index.ts @@ -1,2 +1,2 @@ -export * from "./biometrics.service.abstraction"; +export * from "./desktop.biometrics.service"; export * from "./biometrics.service"; diff --git a/apps/desktop/src/platform/main/desktop-credential-storage-listener.ts b/apps/desktop/src/platform/main/desktop-credential-storage-listener.ts index adc7935e05a..5f278b23a0a 100644 --- a/apps/desktop/src/platform/main/desktop-credential-storage-listener.ts +++ b/apps/desktop/src/platform/main/desktop-credential-storage-listener.ts @@ -6,14 +6,14 @@ import { passwords } from "@bitwarden/desktop-napi"; import { BiometricMessage, BiometricAction } from "../../types/biometric-message"; -import { BiometricsServiceAbstraction } from "./biometric/index"; +import { DesktopBiometricsService } from "./biometric/index"; const AuthRequiredSuffix = "_biometric"; export class DesktopCredentialStorageListener { constructor( private serviceName: string, - private biometricService: BiometricsServiceAbstraction, + private biometricService: DesktopBiometricsService, private logService: ConsoleLogService, ) {} @@ -77,7 +77,16 @@ export class DesktopCredentialStorageListener { }); break; case BiometricAction.OsSupported: - val = await this.biometricService.osSupportsBiometric(); + val = await this.biometricService.supportsBiometric(); + break; + case BiometricAction.NeedsSetup: + val = await this.biometricService.biometricsNeedsSetup(); + break; + case BiometricAction.Setup: + await this.biometricService.biometricsSetup(); + break; + case BiometricAction.CanAutoSetup: + val = await this.biometricService.biometricsSupportsAutoSetup(); break; default: } diff --git a/apps/desktop/src/platform/preload.ts b/apps/desktop/src/platform/preload.ts index d81d6476526..c1c56c5522f 100644 --- a/apps/desktop/src/platform/preload.ts +++ b/apps/desktop/src/platform/preload.ts @@ -11,7 +11,7 @@ import { UnencryptedMessageResponse, } from "../models/native-messaging"; import { BiometricMessage, BiometricAction } from "../types/biometric-message"; -import { isDev, isMacAppStore, isWindowsStore } from "../utils"; +import { isAppImage, isDev, isFlatpak, isMacAppStore, isSnapStore, isWindowsStore } from "../utils"; import { ClipboardWriteMessage } from "./types/clipboard"; @@ -48,6 +48,18 @@ const biometric = { ipcRenderer.invoke("biometric", { action: BiometricAction.OsSupported, } satisfies BiometricMessage), + biometricsNeedsSetup: (): Promise => + ipcRenderer.invoke("biometric", { + action: BiometricAction.NeedsSetup, + } satisfies BiometricMessage), + biometricsSetup: (): Promise => + ipcRenderer.invoke("biometric", { + action: BiometricAction.Setup, + } satisfies BiometricMessage), + biometricsCanAutoSetup: (): Promise => + ipcRenderer.invoke("biometric", { + action: BiometricAction.CanAutoSetup, + } satisfies BiometricMessage), authenticate: (): Promise => ipcRenderer.invoke("biometric", { action: BiometricAction.Authenticate, @@ -59,6 +71,11 @@ const clipboard = { write: (message: ClipboardWriteMessage) => ipcRenderer.invoke("clipboard.write", message), }; +const powermonitor = { + isLockMonitorAvailable: (): Promise => + ipcRenderer.invoke("powermonitor.isLockMonitorAvailable"), +}; + const nativeMessaging = { sendReply: (message: EncryptedMessageResponse | UnencryptedMessageResponse) => { ipcRenderer.send("nativeMessagingReply", message); @@ -94,6 +111,20 @@ const crypto = { ipcRenderer.invoke("crypto.argon2", { password, salt, iterations, memory, parallelism }), }; +const ephemeralStore = { + setEphemeralValue: (key: string, value: string): Promise => + ipcRenderer.invoke("setEphemeralValue", { key, value }), + getEphemeralValue: (key: string): Promise => ipcRenderer.invoke("getEphemeralValue", key), + removeEphemeralValue: (key: string): Promise => + ipcRenderer.invoke("deleteEphemeralValue", key), +}; + +const localhostCallbackService = { + openSsoPrompt: (codeChallenge: string, state: string): Promise => { + return ipcRenderer.invoke("openSsoPrompt", { codeChallenge, state }); + }, +}; + export default { versions: { app: (): Promise => ipcRenderer.invoke("appVersion"), @@ -102,6 +133,9 @@ export default { isDev: isDev(), isMacAppStore: isMacAppStore(), isWindowsStore: isWindowsStore(), + isFlatpak: isFlatpak(), + isSnapStore: isSnapStore(), + isAppImage: isAppImage(), reloadProcess: () => ipcRenderer.send("reload-process"), log: (level: LogLevelType, message?: any, ...optionalParams: any[]) => ipcRenderer.invoke("ipc.log", { level, message, optionalParams }), @@ -148,8 +182,11 @@ export default { passwords, biometric, clipboard, + powermonitor, nativeMessaging, crypto, + ephemeralStore, + localhostCallbackService, }; function deviceType(): DeviceType { diff --git a/apps/desktop/src/platform/services/electron-biometrics.service.ts b/apps/desktop/src/platform/services/electron-biometrics.service.ts new file mode 100644 index 00000000000..8e1b1f8a5d6 --- /dev/null +++ b/apps/desktop/src/platform/services/electron-biometrics.service.ts @@ -0,0 +1,38 @@ +import { Injectable } from "@angular/core"; + +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; + +/** + * This service implement the base biometrics service to provide desktop specific functions, + * specifically for the renderer process by passing messages to the main process. + */ +@Injectable() +export class ElectronBiometricsService extends BiometricsService { + async supportsBiometric(): Promise { + return await ipc.platform.biometric.osSupported(); + } + + async isBiometricUnlockAvailable(): Promise { + return await ipc.platform.biometric.osSupported(); + } + + /** This method is used to authenticate the user presence _only_. + * It should not be used in the process to retrieve + * biometric keys, which has a separate authentication mechanism. + * For biometric keys, invoke "keytar" with a biometric key suffix */ + async authenticateBiometric(): Promise { + return await ipc.platform.biometric.authenticate(); + } + + async biometricsNeedsSetup(): Promise { + return await ipc.platform.biometric.biometricsNeedsSetup(); + } + + async biometricsSupportsAutoSetup(): Promise { + return await ipc.platform.biometric.biometricsCanAutoSetup(); + } + + async biometricsSetup(): Promise { + return await ipc.platform.biometric.biometricsSetup(); + } +} diff --git a/apps/desktop/src/platform/services/electron-crypto.service.spec.ts b/apps/desktop/src/platform/services/electron-crypto.service.spec.ts index 0deeca2d41d..debbd0aa9b4 100644 --- a/apps/desktop/src/platform/services/electron-crypto.service.spec.ts +++ b/apps/desktop/src/platform/services/electron-crypto.service.spec.ts @@ -109,14 +109,6 @@ describe("electronCryptoService", () => { userId: mockUserId, }); }); - - it("clears the old deprecated Biometric key whenever a User Key is set", async () => { - await sut.setUserKey(mockUserKey, mockUserId); - - expect(stateService.setCryptoMasterKeyBiometric).toHaveBeenCalledWith(null, { - userId: mockUserId, - }); - }); }); }); }); diff --git a/apps/desktop/src/platform/services/electron-crypto.service.ts b/apps/desktop/src/platform/services/electron-crypto.service.ts index 1bbd02ab8b9..8a6a51f4c01 100644 --- a/apps/desktop/src/platform/services/electron-crypto.service.ts +++ b/apps/desktop/src/platform/services/electron-crypto.service.ts @@ -13,13 +13,12 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { StateProvider } from "@bitwarden/common/platform/state"; import { CsprngString } from "@bitwarden/common/types/csprng"; import { UserId } from "@bitwarden/common/types/guid"; -import { UserKey, MasterKey } from "@bitwarden/common/types/key"; +import { UserKey } from "@bitwarden/common/types/key"; export class ElectronCryptoService extends CryptoService { constructor( @@ -53,9 +52,7 @@ export class ElectronCryptoService extends CryptoService { override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: UserId): Promise { if (keySuffix === KeySuffixOptions.Biometric) { - // TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3474) - const oldKey = await this.stateService.hasCryptoMasterKeyBiometric({ userId: userId }); - return oldKey || (await this.stateService.hasUserKeyBiometric({ userId: userId })); + return await this.stateService.hasUserKeyBiometric({ userId: userId }); } return super.hasUserKeyStored(keySuffix, userId); } @@ -72,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); @@ -90,7 +87,6 @@ export class ElectronCryptoService extends CryptoService { userId?: UserId, ): Promise { if (keySuffix === KeySuffixOptions.Biometric) { - await this.migrateBiometricKeyIfNeeded(userId); const userKey = await this.stateService.getUserKeyBiometric({ userId: userId }); return userKey == null ? null @@ -149,46 +145,4 @@ export class ElectronCryptoService extends CryptoService { return biometricKey; } - - // --LEGACY METHODS-- - // We previously used the master key for additional keys, but now we use the user key. - // These methods support migrating the old keys to the new ones. - // TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3475) - - override async clearDeprecatedKeys(keySuffix: KeySuffixOptions, userId?: UserId) { - if (keySuffix === KeySuffixOptions.Biometric) { - await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId }); - } - - // 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 - super.clearDeprecatedKeys(keySuffix, userId); - } - - private async migrateBiometricKeyIfNeeded(userId?: UserId) { - if (await this.stateService.hasCryptoMasterKeyBiometric({ userId })) { - const oldBiometricKey = await this.stateService.getCryptoMasterKeyBiometric({ userId }); - // decrypt - const masterKey = new SymmetricCryptoKey(Utils.fromB64ToArray(oldBiometricKey)) as MasterKey; - userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id; - const encUserKeyPrim = await this.stateService.getEncryptedCryptoSymmetricKey({ - userId: userId, - }); - const encUserKey = - encUserKeyPrim != null - ? new EncString(encUserKeyPrim) - : await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId); - if (!encUserKey) { - throw new Error("No user key found during biometric migration"); - } - const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey( - masterKey, - encUserKey, - userId, - ); - // migrate - await this.storeBiometricKey(userKey, userId); - await this.stateService.setCryptoMasterKeyBiometric(null, { userId }); - } - } } diff --git a/apps/desktop/src/platform/services/electron-platform-utils.service.ts b/apps/desktop/src/platform/services/electron-platform-utils.service.ts index 2d50712dfb6..2808b74f097 100644 --- a/apps/desktop/src/platform/services/electron-platform-utils.service.ts +++ b/apps/desktop/src/platform/services/electron-platform-utils.service.ts @@ -131,18 +131,6 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService { return ipc.platform.clipboard.read(); } - async supportsBiometric(): Promise { - return await ipc.platform.biometric.osSupported(); - } - - /** This method is used to authenticate the user presence _only_. - * It should not be used in the process to retrieve - * biometric keys, which has a separate authentication mechanism. - * For biometric keys, invoke "keytar" with a biometric key suffix */ - async authenticateBiometric(): Promise { - return await ipc.platform.biometric.authenticate(); - } - supportsSecureStorage(): boolean { return ELECTRON_SUPPORTS_SECURE_STORAGE; } diff --git a/apps/desktop/src/platform/services/electron-state.service.ts b/apps/desktop/src/platform/services/electron-state.service.ts deleted file mode 100644 index 33c97f48afe..00000000000 --- a/apps/desktop/src/platform/services/electron-state.service.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; -import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service"; - -import { Account } from "../../models/account"; - -export class ElectronStateService extends BaseStateService { - async addAccount(account: Account) { - // Apply desktop overides to default account values - account = new Account(account); - await super.addAccount(account); - } -} diff --git a/apps/desktop/src/platform/services/ephemeral-value-storage.main.service.ts b/apps/desktop/src/platform/services/ephemeral-value-storage.main.service.ts new file mode 100644 index 00000000000..b59b48be1e1 --- /dev/null +++ b/apps/desktop/src/platform/services/ephemeral-value-storage.main.service.ts @@ -0,0 +1,21 @@ +import { ipcMain } from "electron"; + +/** + * The ephemeral value store holds values that should be accessible to the renderer past a process reload. + * In the current state, this store must not contain any keys that can decrypt a vault by themselves. + */ +export class EphemeralValueStorageService { + private ephemeralValues = new Map(); + + constructor() { + ipcMain.handle("setEphemeralValue", async (event, { key, value }) => { + this.ephemeralValues.set(key, value); + }); + ipcMain.handle("getEphemeralValue", async (event, key: string) => { + return this.ephemeralValues.get(key); + }); + ipcMain.handle("deleteEphemeralValue", async (event, key: string) => { + this.ephemeralValues.delete(key); + }); + } +} diff --git a/apps/desktop/src/platform/services/sso-localhost-callback.service.ts b/apps/desktop/src/platform/services/sso-localhost-callback.service.ts new file mode 100644 index 00000000000..cd4c7df66ec --- /dev/null +++ b/apps/desktop/src/platform/services/sso-localhost-callback.service.ts @@ -0,0 +1,134 @@ +import * as http from "http"; + +import { ipcMain } from "electron"; +import { firstValueFrom } from "rxjs"; + +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { MessageSender } from "@bitwarden/common/platform/messaging"; + +/** + * The SSO Localhost login service uses a local host listener as fallback in case scheme handling deeplinks does not work. + * This way it is possible to log in with SSO on appimage, snap, and electron dev using the same methods that the cli uses. + */ +export class SSOLocalhostCallbackService { + private ssoRedirectUri = ""; + + constructor( + private environmentService: EnvironmentService, + private messagingService: MessageSender, + ) { + ipcMain.handle("openSsoPrompt", async (event, { codeChallenge, state }) => { + const { ssoCode, recvState } = await this.openSsoPrompt(codeChallenge, state); + this.messagingService.send("ssoCallback", { + code: ssoCode, + state: recvState, + redirectUri: this.ssoRedirectUri, + }); + }); + } + + private async openSsoPrompt( + codeChallenge: string, + state: string, + ): Promise<{ ssoCode: string; recvState: string }> { + const env = await firstValueFrom(this.environmentService.environment$); + + return new Promise((resolve, reject) => { + const callbackServer = http.createServer((req, res) => { + const urlString = "http://localhost" + req.url; + const url = new URL(urlString); + const code = url.searchParams.get("code"); + if (code == null) { + res.writeHead(404); + res.end("not found"); + return; + } + const receivedState = url.searchParams.get("state"); + res.setHeader("Content-Type", "text/html"); + if (code != null && receivedState != null && this.checkState(receivedState, state)) { + res.writeHead(200); + res.end( + "Success | Bitwarden Desktop" + + "

Successfully authenticated with the Bitwarden desktop app

" + + "

You may now close this tab and return to the app.

" + + "", + ); + callbackServer.close(() => + resolve({ + ssoCode: code, + recvState: receivedState, + }), + ); + } else { + res.writeHead(400); + res.end( + "Failed | Bitwarden Desktop" + + "

Something went wrong logging into the Bitwarden desktop app

" + + "

You may now close this tab and return to the app.

" + + "", + ); + callbackServer.close(() => reject()); + } + }); + + let foundPort = false; + const webUrl = env.getWebVaultUrl(); + for (let port = 8065; port <= 8070; port++) { + try { + this.ssoRedirectUri = "http://localhost:" + port; + callbackServer.listen(port, () => { + this.messagingService.send("launchUri", { + url: + webUrl + + "/#/sso?clientId=" + + "desktop" + + "&redirectUri=" + + encodeURIComponent(this.ssoRedirectUri) + + "&state=" + + state + + "&codeChallenge=" + + codeChallenge, + }); + }); + foundPort = true; + break; + } catch { + // Ignore error since we run the same command up to 5 times. + } + } + if (!foundPort) { + reject(); + } + + // after 5 minutes, close the server + setTimeout( + () => { + callbackServer.close(() => reject()); + }, + 5 * 60 * 1000, + ); + }); + } + + private getOrgIdentifierFromState(state: string): string { + if (state === null || state === undefined) { + return null; + } + + const stateSplit = state.split("_identifier="); + return stateSplit.length > 1 ? stateSplit[1] : null; + } + + private checkState(state: string, checkState: string): boolean { + if (state === null || state === undefined) { + return false; + } + if (checkState === null || checkState === undefined) { + return false; + } + + const stateSplit = state.split("_identifier="); + const checkStateSplit = checkState.split("_identifier="); + return stateSplit[0] === checkStateSplit[0]; + } +} diff --git a/apps/desktop/src/scss/box.scss b/apps/desktop/src/scss/box.scss index 0e89e9fd74e..8c15aa91c62 100644 --- a/apps/desktop/src/scss/box.scss +++ b/apps/desktop/src/scss/box.scss @@ -221,6 +221,7 @@ .txt-right { float: right; margin-left: 10px; + width: 100px; } .row-main { diff --git a/apps/desktop/src/scss/misc.scss b/apps/desktop/src/scss/misc.scss index ccc0af8fa4a..75a72640f2b 100644 --- a/apps/desktop/src/scss/misc.scss +++ b/apps/desktop/src/scss/misc.scss @@ -439,92 +439,6 @@ app-root > #loading, cursor: move; } -.callout { - padding: 10px; - margin-bottom: 10px; - border: 1px solid #000000; - border-left-width: 5px; - border-radius: 3px; - @include themify($themes) { - border-color: themed("calloutBorderColor"); - background-color: themed("calloutBackgroundColor"); - } - - .callout-heading { - margin-top: 0; - } - - h3.callout-heading { - font-weight: bold; - text-transform: uppercase; - } - - &.callout-primary { - @include themify($themes) { - border-left-color: themed("primaryColor"); - } - - .callout-heading { - @include themify($themes) { - color: themed("primaryColor"); - } - } - } - - &.callout-info { - @include themify($themes) { - border-left-color: themed("infoColor"); - } - - .callout-heading { - @include themify($themes) { - color: themed("infoColor"); - } - } - } - - &.callout-danger { - @include themify($themes) { - border-left-color: themed("dangerColor"); - } - - .callout-heading { - @include themify($themes) { - color: themed("dangerColor"); - } - } - } - - &.callout-success { - @include themify($themes) { - border-left-color: themed("successColor"); - } - - .callout-heading { - @include themify($themes) { - color: themed("successColor"); - } - } - } - - &.callout-warning { - @include themify($themes) { - border-left-color: themed("warningColor"); - } - - .callout-heading { - @include themify($themes) { - color: themed("warningColor"); - } - } - } - - ul { - padding-left: 40px; - margin: 0; - } -} - .password-reprompt { text-align: left; margin-top: 15px; diff --git a/apps/desktop/src/scss/plugins.scss b/apps/desktop/src/scss/plugins.scss new file mode 100644 index 00000000000..b8ac8697b7f --- /dev/null +++ b/apps/desktop/src/scss/plugins.scss @@ -0,0 +1,31 @@ +@import "variables.scss"; + +@each $mfaType in $mfaTypes { + .mfaType#{$mfaType} { + content: url("../images/two-factor/" + $mfaType + ".png"); + max-width: 100px; + } +} + +.mfaType1 { + @include themify($themes) { + content: url("../images/two-factor/1" + themed("mfaLogoSuffix")); + max-width: 100px; + max-height: 45px; + } +} + +.mfaType7 { + @include themify($themes) { + content: url("../images/two-factor/7" + themed("mfaLogoSuffix")); + max-width: 100px; + } +} + +.recovery-code-img { + @include themify($themes) { + content: url("../images/two-factor/rc" + themed("mfaLogoSuffix")); + max-width: 100px; + max-height: 45px; + } +} diff --git a/apps/desktop/src/scss/styles.scss b/apps/desktop/src/scss/styles.scss index 54c1385dcf0..187b4bf23c8 100644 --- a/apps/desktop/src/scss/styles.scss +++ b/apps/desktop/src/scss/styles.scss @@ -15,5 +15,6 @@ @import "header.scss"; @import "left-nav.scss"; @import "loading.scss"; +@import "plugins.scss"; @import "../../../../libs/angular/src/scss/icons.scss"; @import "../../../../libs/components/src/multi-select/scss/bw.theme"; diff --git a/apps/desktop/src/scss/variables.scss b/apps/desktop/src/scss/variables.scss index e4a2f124768..6f8112b1ca0 100644 --- a/apps/desktop/src/scss/variables.scss +++ b/apps/desktop/src/scss/variables.scss @@ -15,6 +15,8 @@ $list-icon-color: #c7c7cd; $border-radius: 3px; $line-height-base: 1.42857143; +$mfaTypes: 0, 2, 3, 4, 6; + $gray: #555; $gray-light: #777; $text-muted: $gray-light; @@ -92,6 +94,7 @@ $themes: ( infoColor: $brand-info, warningColor: $brand-warning, logoSuffix: "dark", + mfaLogoSuffix: ".png", passwordNumberColor: #007fde, passwordSpecialColor: #c40800, passwordCountText: #212529, @@ -150,6 +153,7 @@ $themes: ( infoColor: #a4b0c6, warningColor: #ffeb66, logoSuffix: "white", + mfaLogoSuffix: "-w.png", passwordNumberColor: #6f9df1, passwordSpecialColor: #ff8d85, passwordCountText: #ffffff, @@ -208,6 +212,7 @@ $themes: ( infoColor: $nord9, warningColor: $nord12, logoSuffix: "white", + mfaLogoSuffix: "-w.png", passwordNumberColor: $nord8, passwordSpecialColor: $nord12, passwordCountText: $nord5, diff --git a/apps/desktop/src/services/encrypted-message-handler.service.ts b/apps/desktop/src/services/encrypted-message-handler.service.ts index 519bd91064c..535aef307d7 100644 --- a/apps/desktop/src/services/encrypted-message-handler.service.ts +++ b/apps/desktop/src/services/encrypted-message-handler.service.ts @@ -164,7 +164,10 @@ export class EncryptedMessageHandlerService { cipherView.login.uris[0].uri = credentialCreatePayload.uri; try { - const encrypted = await this.cipherService.encrypt(cipherView); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + const encrypted = await this.cipherService.encrypt(cipherView, activeUserId); await this.cipherService.createWithServer(encrypted); // Notify other clients of new login @@ -197,14 +200,17 @@ export class EncryptedMessageHandlerService { if (cipher === null) { return { status: "failure" }; } + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); const cipherView = await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher), + await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), ); cipherView.name = credentialUpdatePayload.name; cipherView.login.password = credentialUpdatePayload.password; cipherView.login.username = credentialUpdatePayload.userName; cipherView.login.uris[0].uri = credentialUpdatePayload.uri; - const encrypted = await this.cipherService.encrypt(cipherView); + const encrypted = await this.cipherService.encrypt(cipherView, activeUserId); await this.cipherService.updateWithServer(encrypted); diff --git a/apps/desktop/src/services/native-messaging.service.ts b/apps/desktop/src/services/native-messaging.service.ts index 7f6d39b2e8d..f106d137b76 100644 --- a/apps/desktop/src/services/native-messaging.service.ts +++ b/apps/desktop/src/services/native-messaging.service.ts @@ -3,14 +3,13 @@ import { firstValueFrom, map } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; +import { BiometricsService } from "@bitwarden/common/platform/biometrics/biometric.service"; import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -31,17 +30,14 @@ const HashAlgorithmForAsymmetricEncryption = "sha1"; @Injectable() export class NativeMessagingService { - private sharedSecrets = new Map(); - constructor( - private masterPasswordService: MasterPasswordServiceAbstraction, private cryptoFunctionService: CryptoFunctionService, private cryptoService: CryptoService, - private platformUtilService: PlatformUtilsService, private logService: LogService, private messagingService: MessagingService, private desktopSettingService: DesktopSettingsService, private biometricStateService: BiometricStateService, + private biometricsService: BiometricsService, private nativeMessageHandler: NativeMessageHandlerService, private dialogService: DialogService, private accountService: AccountService, @@ -106,7 +102,7 @@ export class NativeMessagingService { return; } - if (this.sharedSecrets.get(appId) == null) { + if ((await ipc.platform.ephemeralStore.getEphemeralValue(appId)) == null) { ipc.platform.nativeMessaging.sendMessage({ command: "invalidateEncryption", appId: appId, @@ -117,7 +113,7 @@ export class NativeMessagingService { const message: LegacyMessage = JSON.parse( await this.cryptoService.decryptToUtf8( rawMessage as EncString, - this.sharedSecrets.get(appId), + SymmetricCryptoKey.fromString(await ipc.platform.ephemeralStore.getEphemeralValue(appId)), ), ); @@ -137,7 +133,14 @@ export class NativeMessagingService { switch (message.command) { case "biometricUnlock": { - if (!(await this.platformUtilService.supportsBiometric())) { + const isTemporarilyDisabled = + (await this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId)) && + !(await this.biometricsService.supportsBiometric()); + if (isTemporarilyDisabled) { + return this.send({ command: "biometricUnlock", response: "not available" }, appId); + } + + if (!(await this.biometricsService.supportsBiometric())) { return this.send({ command: "biometricUnlock", response: "not supported" }, appId); } @@ -149,11 +152,6 @@ export class NativeMessagingService { return this.send({ command: "biometricUnlock", response: "not unlocked" }, appId); } - const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId)); - if (authStatus !== AuthenticationStatus.Unlocked) { - return this.send({ command: "biometricUnlock", response: "not unlocked" }, appId); - } - const biometricUnlockPromise = message.userId == null ? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$) @@ -177,23 +175,27 @@ export class NativeMessagingService { KeySuffixOptions.Biometric, message.userId, ); - const masterKey = await firstValueFrom( - this.masterPasswordService.masterKey$(message.userId as UserId), - ); if (userKey != null) { - // we send the master key still for backwards compatibility - // with older browser extensions - // TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3472) await this.send( { command: "biometricUnlock", response: "unlocked", - keyB64: masterKey?.keyB64, userKeyB64: userKey.keyB64, }, appId, ); + + const currentlyActiveAccountId = ( + await firstValueFrom(this.accountService.activeAccount$) + ).id; + const isCurrentlyActiveAccountUnlocked = + (await this.authService.getAuthStatus(userId)) == AuthenticationStatus.Unlocked; + + // prevent proc reloading an active account, when it is the same as the browser + if (currentlyActiveAccountId != message.userId || !isCurrentlyActiveAccountUnlocked) { + await ipc.platform.reloadProcess(); + } } else { await this.send({ command: "biometricUnlock", response: "canceled" }, appId); } @@ -203,8 +205,18 @@ export class NativeMessagingService { break; } + case "biometricUnlockAvailable": { + const isAvailable = await this.biometricsService.supportsBiometric(); + return this.send( + { + command: "biometricUnlockAvailable", + response: isAvailable ? "available" : "not available", + }, + appId, + ); + } default: - this.logService.error("NativeMessage, got unknown command."); + this.logService.error("NativeMessage, got unknown command: " + message.command); break; } } @@ -214,7 +226,7 @@ export class NativeMessagingService { const encrypted = await this.cryptoService.encrypt( JSON.stringify(message), - this.sharedSecrets.get(appId), + SymmetricCryptoKey.fromString(await ipc.platform.ephemeralStore.getEphemeralValue(appId)), ); ipc.platform.nativeMessaging.sendMessage({ appId: appId, message: encrypted }); @@ -222,7 +234,10 @@ export class NativeMessagingService { private async secureCommunication(remotePublicKey: Uint8Array, appId: string) { const secret = await this.cryptoFunctionService.randomBytes(64); - this.sharedSecrets.set(appId, new SymmetricCryptoKey(secret)); + await ipc.platform.ephemeralStore.setEphemeralValue( + appId, + new SymmetricCryptoKey(secret).keyB64, + ); const encryptedSecret = await this.cryptoFunctionService.rsaEncrypt( secret, diff --git a/apps/desktop/src/types/biometric-message.ts b/apps/desktop/src/types/biometric-message.ts index 001aabc1fe2..0db7b60a2df 100644 --- a/apps/desktop/src/types/biometric-message.ts +++ b/apps/desktop/src/types/biometric-message.ts @@ -2,6 +2,9 @@ export enum BiometricAction { EnabledForUser = "enabled", OsSupported = "osSupported", Authenticate = "authenticate", + NeedsSetup = "needsSetup", + Setup = "setup", + CanAutoSetup = "canAutoSetup", } export type BiometricMessage = { diff --git a/apps/desktop/src/utils.ts b/apps/desktop/src/utils.ts index 78011c16ed8..98bdebb0cc3 100644 --- a/apps/desktop/src/utils.ts +++ b/apps/desktop/src/utils.ts @@ -62,6 +62,10 @@ export function isWindowsStore() { return windows && windowsStore === true; } +export function isFlatpak() { + return process.platform === "linux" && process.env.container != null; +} + export function isWindowsPortable() { return isWindows() && process.env.PORTABLE_EXECUTABLE_DIR != null; } diff --git a/apps/desktop/src/vault/app/accounts/premium.component.ts b/apps/desktop/src/vault/app/accounts/premium.component.ts index 8b3d9e11f02..373e5d88177 100644 --- a/apps/desktop/src/vault/app/accounts/premium.component.ts +++ b/apps/desktop/src/vault/app/accounts/premium.component.ts @@ -3,6 +3,7 @@ import { Component } from "@angular/core"; import { PremiumComponent as BasePremiumComponent } from "@bitwarden/angular/vault/components/premium.component"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -19,6 +20,7 @@ export class PremiumComponent extends BasePremiumComponent { i18nService: I18nService, platformUtilsService: PlatformUtilsService, apiService: ApiService, + configService: ConfigService, logService: LogService, stateService: StateService, dialogService: DialogService, @@ -29,8 +31,8 @@ export class PremiumComponent extends BasePremiumComponent { i18nService, platformUtilsService, apiService, + configService, logService, - stateService, dialogService, environmentService, billingAccountProfileStateService, diff --git a/apps/desktop/src/vault/app/vault/add-edit.component.ts b/apps/desktop/src/vault/app/vault/add-edit.component.ts index d7fd3947953..098316de9ec 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.ts +++ b/apps/desktop/src/vault/app/vault/add-edit.component.ts @@ -1,5 +1,5 @@ import { DatePipe } from "@angular/common"; -import { Component, NgZone, OnChanges, OnDestroy, ViewChild } from "@angular/core"; +import { Component, NgZone, OnChanges, OnInit, OnDestroy, ViewChild } from "@angular/core"; import { NgForm } from "@angular/forms"; import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component"; @@ -27,7 +27,7 @@ const BroadcasterSubscriptionId = "AddEditComponent"; selector: "app-vault-add-edit", templateUrl: "add-edit.component.html", }) -export class AddEditComponent extends BaseAddEditComponent implements OnChanges, OnDestroy { +export class AddEditComponent extends BaseAddEditComponent implements OnInit, OnChanges, OnDestroy { @ViewChild("form") private form: NgForm; constructor( diff --git a/apps/desktop/src/vault/app/vault/attachments.component.ts b/apps/desktop/src/vault/app/vault/attachments.component.ts index 8066da89a2e..2e25d390872 100644 --- a/apps/desktop/src/vault/app/vault/attachments.component.ts +++ b/apps/desktop/src/vault/app/vault/attachments.component.ts @@ -2,6 +2,7 @@ import { Component } from "@angular/core"; import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; @@ -10,7 +11,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Component({ selector: "app-vault-attachments", @@ -28,6 +29,8 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { fileDownloadService: FileDownloadService, dialogService: DialogService, billingAccountProfileStateService: BillingAccountProfileStateService, + accountService: AccountService, + toastService: ToastService, ) { super( cipherService, @@ -41,6 +44,8 @@ export class AttachmentsComponent extends BaseAttachmentsComponent { fileDownloadService, dialogService, billingAccountProfileStateService, + accountService, + toastService, ); } } diff --git a/apps/desktop/src/vault/app/vault/collections.component.ts b/apps/desktop/src/vault/app/vault/collections.component.ts index 4b6a88f325a..3885ca00578 100644 --- a/apps/desktop/src/vault/app/vault/collections.component.ts +++ b/apps/desktop/src/vault/app/vault/collections.component.ts @@ -2,12 +2,13 @@ import { Component } from "@angular/core"; import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-vault-collections", @@ -21,7 +22,8 @@ export class CollectionsComponent extends BaseCollectionsComponent { platformUtilsService: PlatformUtilsService, organizationService: OrganizationService, logService: LogService, - configService: ConfigService, + accountService: AccountService, + toastService: ToastService, ) { super( collectionService, @@ -30,7 +32,8 @@ export class CollectionsComponent extends BaseCollectionsComponent { cipherService, organizationService, logService, - configService, + accountService, + toastService, ); } } diff --git a/apps/desktop/src/vault/app/vault/password-history.component.ts b/apps/desktop/src/vault/app/vault/password-history.component.ts index 44e2198a15d..12701ac5527 100644 --- a/apps/desktop/src/vault/app/vault/password-history.component.ts +++ b/apps/desktop/src/vault/app/vault/password-history.component.ts @@ -1,6 +1,7 @@ import { Component } from "@angular/core"; import { PasswordHistoryComponent as BasePasswordHistoryComponent } from "@bitwarden/angular/vault/components/password-history.component"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -14,7 +15,8 @@ export class PasswordHistoryComponent extends BasePasswordHistoryComponent { cipherService: CipherService, platformUtilsService: PlatformUtilsService, i18nService: I18nService, + accountService: AccountService, ) { - super(cipherService, platformUtilsService, i18nService, window); + super(cipherService, platformUtilsService, i18nService, accountService, window); } } diff --git a/apps/desktop/src/vault/app/vault/share.component.ts b/apps/desktop/src/vault/app/vault/share.component.ts index 95b22386e45..ddaad8337bc 100644 --- a/apps/desktop/src/vault/app/vault/share.component.ts +++ b/apps/desktop/src/vault/app/vault/share.component.ts @@ -3,6 +3,7 @@ import { Component } from "@angular/core"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { ShareComponent as BaseShareComponent } from "@bitwarden/angular/components/share.component"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -21,6 +22,7 @@ export class ShareComponent extends BaseShareComponent { platformUtilsService: PlatformUtilsService, logService: LogService, organizationService: OrganizationService, + accountService: AccountService, private modalRef: ModalRef, ) { super( @@ -30,6 +32,7 @@ export class ShareComponent extends BaseShareComponent { cipherService, logService, organizationService, + accountService, ); } diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/collection-filter.component.html b/apps/desktop/src/vault/app/vault/vault-filter/filters/collection-filter.component.html index 28c815d8371..667cdf6798c 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/collection-filter.component.html +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/collection-filter.component.html @@ -13,7 +13,7 @@ aria-hidden="true" [ngClass]="{ 'bwi-angle-right': isCollapsed(collectionsGrouping), - 'bwi-angle-down': !isCollapsed(collectionsGrouping) + 'bwi-angle-down': !isCollapsed(collectionsGrouping), }" >  {{ collectionsGrouping.name | i18n }} @@ -42,7 +42,7 @@ aria-hidden="true" [ngClass]="{ 'bwi-angle-right': isCollapsed(c.node), - 'bwi-angle-down': !isCollapsed(c.node) + 'bwi-angle-down': !isCollapsed(c.node), }" > diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/folder-filter.component.html b/apps/desktop/src/vault/app/vault/vault-filter/filters/folder-filter.component.html index 218a4c12692..a2240b03ff5 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/folder-filter.component.html +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/folder-filter.component.html @@ -13,7 +13,7 @@ aria-hidden="true" [ngClass]="{ 'bwi-angle-right': isCollapsed(foldersGrouping), - 'bwi-angle-down': !isCollapsed(foldersGrouping) + 'bwi-angle-down': !isCollapsed(foldersGrouping), }" >  {{ foldersGrouping.name | i18n }} @@ -33,7 +33,7 @@
  • @@ -52,7 +52,7 @@ aria-hidden="true" [ngClass]="{ 'bwi-angle-right': isCollapsed(f.node), - 'bwi-angle-down': !isCollapsed(f.node) + 'bwi-angle-down': !isCollapsed(f.node), }" > diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.html b/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.html index 2740b229781..f77f279a96f 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.html +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/organization-filter.component.html @@ -15,7 +15,7 @@ aria-hidden="true" [ngClass]="{ 'bwi-angle-right': isCollapsed, - 'bwi-angle-down': !isCollapsed + 'bwi-angle-down': !isCollapsed, }" > @@ -74,7 +74,7 @@ aria-hidden="true" [ngClass]="{ 'bwi-angle-right': isCollapsed, - 'bwi-angle-down': !isCollapsed + 'bwi-angle-down': !isCollapsed, }" > diff --git a/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.html b/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.html index 1df87a69ed6..381c06e8b67 100644 --- a/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.html +++ b/apps/desktop/src/vault/app/vault/vault-filter/filters/type-filter.component.html @@ -12,7 +12,7 @@ aria-hidden="true" [ngClass]="{ 'bwi-angle-right': isCollapsed, - 'bwi-angle-down': !isCollapsed + 'bwi-angle-down': !isCollapsed, }" >  {{ typesNode.name | i18n }} diff --git a/apps/desktop/src/vault/app/vault/view.component.ts b/apps/desktop/src/vault/app/vault/view.component.ts index 0db12d1ba8d..140e1e9ced6 100644 --- a/apps/desktop/src/vault/app/vault/view.component.ts +++ b/apps/desktop/src/vault/app/vault/view.component.ts @@ -5,6 +5,8 @@ import { EventEmitter, NgZone, OnChanges, + OnDestroy, + OnInit, Output, } from "@angular/core"; @@ -12,6 +14,7 @@ import { ViewComponent as BaseViewComponent } from "@bitwarden/angular/vault/com import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; @@ -35,7 +38,7 @@ const BroadcasterSubscriptionId = "ViewComponent"; selector: "app-vault-view", templateUrl: "view.component.html", }) -export class ViewComponent extends BaseViewComponent implements OnChanges { +export class ViewComponent extends BaseViewComponent implements OnInit, OnDestroy, OnChanges { @Output() onViewCipherPasswordHistory = new EventEmitter(); constructor( @@ -60,6 +63,7 @@ export class ViewComponent extends BaseViewComponent implements OnChanges { dialogService: DialogService, datePipe: DatePipe, billingAccountProfileStateService: BillingAccountProfileStateService, + accountService: AccountService, ) { super( cipherService, @@ -82,6 +86,7 @@ export class ViewComponent extends BaseViewComponent implements OnChanges { fileDownloadService, dialogService, datePipe, + accountService, billingAccountProfileStateService, ); } diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index 5ffdb3c2076..19f7b8bf70f 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -10,7 +10,7 @@ "types": [], "baseUrl": ".", "paths": { - "@bitwarden/admin-console": ["../../libs/admin-console/src"], + "@bitwarden/admin-console/common": ["../../libs/admin-console/src/common"], "@bitwarden/angular/*": ["../../libs/angular/src/*"], "@bitwarden/auth/common": ["../../libs/auth/src/common"], "@bitwarden/auth/angular": ["../../libs/auth/src/angular"], diff --git a/apps/web/config/base.json b/apps/web/config/base.json index b9102a769d7..8eb8a311335 100644 --- a/apps/web/config/base.json +++ b/apps/web/config/base.json @@ -11,7 +11,6 @@ "allowedHosts": "auto" }, "flags": { - "showPasswordless": false, - "enableCipherKeyEncryption": true + "showPasswordless": false } } diff --git a/apps/web/config/cloud.json b/apps/web/config/cloud.json index c8ba07e755e..8817142c9ed 100644 --- a/apps/web/config/cloud.json +++ b/apps/web/config/cloud.json @@ -17,7 +17,6 @@ "proxyNotifications": "https://notifications.bitwarden.com" }, "flags": { - "showPasswordless": true, - "enableCipherKeyEncryption": true + "showPasswordless": true } } diff --git a/apps/web/config/development.json b/apps/web/config/development.json index 3fcd8641b32..58dec82a154 100644 --- a/apps/web/config/development.json +++ b/apps/web/config/development.json @@ -20,8 +20,7 @@ } ], "flags": { - "showPasswordless": true, - "enableCipherKeyEncryption": true + "showPasswordless": true }, "devFlags": {} } diff --git a/apps/web/config/euprd.json b/apps/web/config/euprd.json index 2d554e57043..99d98ca09dd 100644 --- a/apps/web/config/euprd.json +++ b/apps/web/config/euprd.json @@ -11,7 +11,6 @@ "buttonAction": "https://www.paypal.com/cgi-bin/webscr" }, "flags": { - "showPasswordless": true, - "enableCipherKeyEncryption": true + "showPasswordless": true } } diff --git a/apps/web/config/qa.json b/apps/web/config/qa.json index f03d47fe4ee..07e341e6f9f 100644 --- a/apps/web/config/qa.json +++ b/apps/web/config/qa.json @@ -27,7 +27,6 @@ } ], "flags": { - "showPasswordless": true, - "enableCipherKeyEncryption": true + "showPasswordless": true } } diff --git a/apps/web/config/selfhosted.json b/apps/web/config/selfhosted.json index 121f59ba0b3..9d8e1cf2685 100644 --- a/apps/web/config/selfhosted.json +++ b/apps/web/config/selfhosted.json @@ -7,7 +7,6 @@ "port": 8081 }, "flags": { - "showPasswordless": true, - "enableCipherKeyEncryption": true + "showPasswordless": true } } diff --git a/apps/web/package.json b/apps/web/package.json index 11bf27b4e39..08b8d182837 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.7.0", + "version": "2024.9.0", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/apps/web/src/404.html b/apps/web/src/404.html index 061cdc0f47a..817bfe30985 100644 --- a/apps/web/src/404.html +++ b/apps/web/src/404.html @@ -1,52 +1,37 @@ - + - - - - - - - - - - Page not found! + Page not found | Bitwarden Web vault + + + + + + - - -
    -

    Page not found!

    -

    Sorry, but the page you were looking for could not be found.

    -

    - - 404 image - + + Bitwarden + +

    +

    Sorry, this page isn't available.

    + +

    + The link you followed may be broken, or the page may have been removed. Try going back to + the previous page or see our + Help Center for + more information.

    -

    - You can return to the web vault, check our - status page or - contact us. -

    -
    - + + Go to your web vault + +
    + +
    diff --git a/apps/web/src/404/bootstrap.min.css b/apps/web/src/404/bootstrap.min.css deleted file mode 100644 index 282380667eb..00000000000 --- a/apps/web/src/404/bootstrap.min.css +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * Bootstrap v4.6.0 (https://getbootstrap.com/) - * Copyright 2011-2021 The Bootstrap Authors - * Copyright 2011-2021 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit;text-align:-webkit-match-parent}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-sm-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-sm-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-md-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-md-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-lg-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-lg-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.row-cols-xl-1>*{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.row-cols-xl-4>*{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;margin-bottom:1rem;color:#212529}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-primary tbody+tbody,.table-primary td,.table-primary th,.table-primary thead th{border-color:#7abaff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-secondary tbody+tbody,.table-secondary td,.table-secondary th,.table-secondary thead th{border-color:#b3b7bb}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-success tbody+tbody,.table-success td,.table-success th,.table-success thead th{border-color:#8fd19e}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-info tbody+tbody,.table-info td,.table-info th,.table-info thead th{border-color:#86cfda}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-warning tbody+tbody,.table-warning td,.table-warning th,.table-warning thead th{border-color:#ffdf7e}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-danger tbody+tbody,.table-danger td,.table-danger th,.table-danger thead th{border-color:#ed969e}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-light tbody+tbody,.table-light td,.table-light th,.table-light thead th{border-color:#fbfcfc}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#95999c}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark td,.table-dark th,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{-webkit-appearance:none;-moz-appearance:none;appearance:none}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;font-size:1rem;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[multiple],select.form-control[size]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(40,167,69,.9);border-radius:.25rem}.form-row>.col>.valid-tooltip,.form-row>[class*=col-]>.valid-tooltip{left:5px}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#28a745;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-valid,.was-validated .custom-select:valid{border-color:#28a745;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat,#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2328a745' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat}.custom-select.is-valid:focus,.was-validated .custom-select:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{border-color:#28a745}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{border-color:#34ce57;background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-control-input.is-valid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:valid:focus:not(:checked)~.custom-control-label::before{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;left:0;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.form-row>.col>.invalid-tooltip,.form-row>[class*=col-]>.invalid-tooltip{left:5px}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.custom-select.is-invalid,.was-validated .custom-select:invalid{border-color:#dc3545;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat,#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23dc3545' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e") center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem) no-repeat}.custom-select.is-invalid:focus,.was-validated .custom-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{border-color:#dc3545}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{border-color:#e4606d;background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-control-input.is-invalid:focus:not(:checked)~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus:not(:checked)~.custom-control-label::before{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#0069d9;border-color:#0062cc;box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(38,143,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{color:#fff;background-color:#5a6268;border-color:#545b62;box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(130,138,145,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#218838;border-color:#1e7e34;box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(72,180,97,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#138496;border-color:#117a8b;box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(58,176,195,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{color:#212529;background-color:#e0a800;border-color:#d39e00;box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(222,170,12,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c82333;border-color:#bd2130;box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(225,83,97,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{color:#212529;background-color:#e2e6ea;border-color:#dae0e5;box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,217,219,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{color:#fff;background-color:#23272b;border-color:#1d2124;box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(82,88,93,.5)}.btn-outline-primary{color:#007bff;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;text-decoration:none}.btn-link:hover{color:#0056b3;text-decoration:underline}.btn-link.focus,.btn-link:focus{text-decoration:underline}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control,.input-group>.form-control-plaintext{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;min-width:0;margin-bottom:0}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control,.input-group>.form-control-plaintext+.custom-file,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.form-control{margin-left:-1px}.input-group>.custom-file .custom-file-input:focus~.custom-file-label,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:first-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group:not(.has-validation)>.custom-file:not(:last-child) .custom-file-label::after,.input-group:not(.has-validation)>.custom-select:not(:last-child),.input-group:not(.has-validation)>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.custom-file:nth-last-child(n+3) .custom-file-label::after,.input-group.has-validation>.custom-select:nth-last-child(n+3),.input-group.has-validation>.form-control:nth-last-child(n+3){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn:focus,.input-group-prepend .btn:focus{z-index:3}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group-lg>.custom-select,.input-group-lg>.form-control:not(textarea){height:calc(1.5em + 1rem + 2px)}.input-group-lg>.custom-select,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.custom-select,.input-group-sm>.form-control:not(textarea){height:calc(1.5em + .5rem + 2px)}.input-group-sm>.custom-select,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group.has-validation>.input-group-append:nth-last-child(n+3)>.btn,.input-group.has-validation>.input-group-append:nth-last-child(n+3)>.input-group-text,.input-group:not(.has-validation)>.input-group-append:not(:last-child)>.btn,.input-group:not(.has-validation)>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;z-index:1;display:block;min-height:1.5rem;padding-left:1.5rem;-webkit-print-color-adjust:exact;color-adjust:exact}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.25rem;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;border-color:#007bff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:focus:not(:checked)~.custom-control-label::before{border-color:#80bdff}.custom-control-input:not(:disabled):active~.custom-control-label::before{color:#fff;background-color:#b3d7ff;border-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label,.custom-control-input[disabled]~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before,.custom-control-input[disabled]~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:50%/50% 50% no-repeat}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{border-color:#007bff;background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:transform .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-transform .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked~.custom-control-label::after{background-color:#fff;-webkit-transform:translateX(.75rem);transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") right .75rem center/8px 10px no-repeat;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;overflow:hidden;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:disabled~.custom-file-label,.custom-file-input[disabled]~.custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-input~.custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;overflow:hidden;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-moz-range-thumb{-moz-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#007bff;border:0;border-radius:1rem;-ms-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.custom-range::-ms-thumb{-ms-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item,.nav-fill>.nav-link{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-lg,.navbar .container-md,.navbar .container-sm,.navbar .container-xl{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:50%/100% 100% no-repeat}.navbar-nav-scroll{max-height:75vh;overflow-y:auto}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-md,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-lg,.navbar-expand>.container-md,.navbar-expand>.container-sm,.navbar-expand>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{-ms-flex-negative:0;flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{-ms-flex:1 0 0%;flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion{overflow-anchor:none}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.badge{transition:none}}a.badge:focus,a.badge:hover{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}a.badge-primary:focus,a.badge-primary:hover{color:#fff;background-color:#0062cc}a.badge-primary.focus,a.badge-primary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.badge-secondary{color:#fff;background-color:#6c757d}a.badge-secondary:focus,a.badge-secondary:hover{color:#fff;background-color:#545b62}a.badge-secondary.focus,a.badge-secondary:focus{outline:0;box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.badge-success{color:#fff;background-color:#28a745}a.badge-success:focus,a.badge-success:hover{color:#fff;background-color:#1e7e34}a.badge-success.focus,a.badge-success:focus{outline:0;box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.badge-info{color:#fff;background-color:#17a2b8}a.badge-info:focus,a.badge-info:hover{color:#fff;background-color:#117a8b}a.badge-info.focus,a.badge-info:focus{outline:0;box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.badge-warning{color:#212529;background-color:#ffc107}a.badge-warning:focus,a.badge-warning:hover{color:#212529;background-color:#d39e00}a.badge-warning.focus,a.badge-warning:focus{outline:0;box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.badge-danger{color:#fff;background-color:#dc3545}a.badge-danger:focus,a.badge-danger:hover{color:#fff;background-color:#bd2130}a.badge-danger.focus,a.badge-danger:focus{outline:0;box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.badge-light{color:#212529;background-color:#f8f9fa}a.badge-light:focus,a.badge-light:hover{color:#212529;background-color:#dae0e5}a.badge-light.focus,a.badge-light:focus{outline:0;box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.badge-dark{color:#fff;background-color:#343a40}a.badge-dark:focus,a.badge-dark:hover{color:#fff;background-color:#1d2124}a.badge-dark.focus,a.badge-dark:focus{outline:0;box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;z-index:2;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;line-height:0;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):focus,.close:not(:disabled):not(.disabled):hover{opacity:.75}button.close{padding:0;background-color:transparent;border:0}a.close.disabled{pointer-events:none}.toast{-ms-flex-preferred-size:350px;flex-basis:350px;max-width:350px;font-size:.875rem;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .25rem .75rem rgba(0,0,0,.1);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-50px);transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal.modal-static .modal-dialog{-webkit-transform:scale(1.02);transform:scale(1.02)}.modal-dialog-scrollable{display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-footer,.modal-dialog-scrollable .modal-header{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);height:-webkit-min-content;height:-moz-min-content;height:min-content;content:""}.modal-dialog-centered.modal-dialog-scrollable{-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem);height:-webkit-min-content;height:-moz-min-content;height:min-content}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top]>.arrow,.bs-popover-top>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=top]>.arrow::before,.bs-popover-top>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top]>.arrow::after,.bs-popover-top>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right]>.arrow,.bs-popover-right>.arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right]>.arrow::before,.bs-popover-right>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right]>.arrow::after,.bs-popover-right>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom]>.arrow,.bs-popover-bottom>.arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[x-placement^=bottom]>.arrow::before,.bs-popover-bottom>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom]>.arrow::after,.bs-popover-bottom>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left]>.arrow,.bs-popover-left>.arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left]>.arrow::before,.bs-popover-left>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left]>.arrow::after,.bs-popover-left>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-right,.carousel-item-next:not(.carousel-item-left){-webkit-transform:translateX(100%);transform:translateX(100%)}.active.carousel-item-left,.carousel-item-prev:not(.carousel-item-right){-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:50%/100% 100% no-repeat}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded-sm{border-radius:.2rem!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-lg{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;-ms-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important;user-select:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.min-vw-100{min-width:100vw!important}.min-vh-100{min-height:100vh!important}.vw-100{width:100vw!important}.vh-100{height:100vh!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-n1{margin:-.25rem!important}.mt-n1,.my-n1{margin-top:-.25rem!important}.mr-n1,.mx-n1{margin-right:-.25rem!important}.mb-n1,.my-n1{margin-bottom:-.25rem!important}.ml-n1,.mx-n1{margin-left:-.25rem!important}.m-n2{margin:-.5rem!important}.mt-n2,.my-n2{margin-top:-.5rem!important}.mr-n2,.mx-n2{margin-right:-.5rem!important}.mb-n2,.my-n2{margin-bottom:-.5rem!important}.ml-n2,.mx-n2{margin-left:-.5rem!important}.m-n3{margin:-1rem!important}.mt-n3,.my-n3{margin-top:-1rem!important}.mr-n3,.mx-n3{margin-right:-1rem!important}.mb-n3,.my-n3{margin-bottom:-1rem!important}.ml-n3,.mx-n3{margin-left:-1rem!important}.m-n4{margin:-1.5rem!important}.mt-n4,.my-n4{margin-top:-1.5rem!important}.mr-n4,.mx-n4{margin-right:-1.5rem!important}.mb-n4,.my-n4{margin-bottom:-1.5rem!important}.ml-n4,.mx-n4{margin-left:-1.5rem!important}.m-n5{margin:-3rem!important}.mt-n5,.my-n5{margin-top:-3rem!important}.mr-n5,.mx-n5{margin-right:-3rem!important}.mb-n5,.my-n5{margin-bottom:-3rem!important}.ml-n5,.mx-n5{margin-left:-3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-n1{margin:-.25rem!important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem!important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem!important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem!important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem!important}.m-sm-n2{margin:-.5rem!important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem!important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem!important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem!important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem!important}.m-sm-n3{margin:-1rem!important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem!important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem!important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem!important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem!important}.m-sm-n4{margin:-1.5rem!important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem!important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem!important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem!important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem!important}.m-sm-n5{margin:-3rem!important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem!important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem!important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem!important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-n1{margin:-.25rem!important}.mt-md-n1,.my-md-n1{margin-top:-.25rem!important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem!important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem!important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem!important}.m-md-n2{margin:-.5rem!important}.mt-md-n2,.my-md-n2{margin-top:-.5rem!important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem!important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem!important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem!important}.m-md-n3{margin:-1rem!important}.mt-md-n3,.my-md-n3{margin-top:-1rem!important}.mr-md-n3,.mx-md-n3{margin-right:-1rem!important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem!important}.ml-md-n3,.mx-md-n3{margin-left:-1rem!important}.m-md-n4{margin:-1.5rem!important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem!important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem!important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem!important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem!important}.m-md-n5{margin:-3rem!important}.mt-md-n5,.my-md-n5{margin-top:-3rem!important}.mr-md-n5,.mx-md-n5{margin-right:-3rem!important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem!important}.ml-md-n5,.mx-md-n5{margin-left:-3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-n1{margin:-.25rem!important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem!important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem!important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem!important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem!important}.m-lg-n2{margin:-.5rem!important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem!important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem!important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem!important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem!important}.m-lg-n3{margin:-1rem!important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem!important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem!important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem!important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem!important}.m-lg-n4{margin:-1.5rem!important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem!important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem!important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem!important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem!important}.m-lg-n5{margin:-3rem!important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem!important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem!important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem!important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-n1{margin:-.25rem!important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem!important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem!important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem!important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem!important}.m-xl-n2{margin:-.5rem!important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem!important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem!important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem!important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem!important}.m-xl-n3{margin:-1rem!important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem!important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem!important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem!important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem!important}.m-xl-n4{margin:-1.5rem!important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem!important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem!important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem!important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem!important}.m-xl-n5{margin:-3rem!important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem!important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem!important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem!important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace!important}.text-justify{text-align:justify!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-lighter{font-weight:lighter!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-weight-bolder{font-weight:bolder!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0056b3!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#494f54!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#19692c!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#0f6674!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#ba8b00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#a71d2a!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#cbd3da!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#121416!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none!important}.text-break{word-break:break-word!important;word-wrap:break-word!important}.text-reset{color:inherit!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}} -/*# sourceMappingURL=bootstrap.min.css.map */ diff --git a/apps/web/src/404/styles.css b/apps/web/src/404/styles.css deleted file mode 100644 index 0ef36e5219a..00000000000 --- a/apps/web/src/404/styles.css +++ /dev/null @@ -1,154 +0,0 @@ -@font-face { - font-family: "Open Sans"; - font-style: italic; - font-weight: 300; - src: url(../fonts/Open_Sans-italic-300.woff) format("woff"); - unicode-range: U+0-10FFFF; -} - -@font-face { - font-family: "Open Sans"; - font-style: italic; - font-weight: 400; - src: url(../fonts/Open_Sans-italic-400.woff) format("woff"); - unicode-range: U+0-10FFFF; -} - -@font-face { - font-family: "Open Sans"; - font-style: italic; - font-weight: 600; - src: url(../fonts/Open_Sans-italic-600.woff) format("woff"); - unicode-range: U+0-10FFFF; -} - -@font-face { - font-family: "Open Sans"; - font-style: italic; - font-weight: 700; - src: url(../fonts/Open_Sans-italic-700.woff) format("woff"); - unicode-range: U+0-10FFFF; -} - -@font-face { - font-family: "Open Sans"; - font-style: italic; - font-weight: 800; - src: url(../fonts/Open_Sans-italic-800.woff) format("woff"); - unicode-range: U+0-10FFFF; -} - -@font-face { - font-family: "Open Sans"; - font-style: normal; - font-weight: 300; - src: url(../fonts/Open_Sans-normal-300.woff) format("woff"); - unicode-range: U+0-10FFFF; -} - -@font-face { - font-family: "Open Sans"; - font-style: normal; - font-weight: 400; - src: url(../fonts/Open_Sans-normal-400.woff) format("woff"); - unicode-range: U+0-10FFFF; -} - -@font-face { - font-family: "Open Sans"; - font-style: normal; - font-weight: 600; - src: url(../fonts/Open_Sans-normal-600.woff) format("woff"); - unicode-range: U+0-10FFFF; -} - -@font-face { - font-family: "Open Sans"; - font-style: normal; - font-weight: 700; - src: url(../fonts/Open_Sans-normal-700.woff) format("woff"); - unicode-range: U+0-10FFFF; -} - -@font-face { - font-family: "Open Sans"; - font-style: normal; - font-weight: 800; - src: url(../fonts/Open_Sans-normal-800.woff) format("woff"); - unicode-range: U+0-10FFFF; -} - -body { - font-family: "Open Sans"; -} - -html, -body, -.row { - height: 100%; - -webkit-font-smoothing: antialiased; -} - -h2 { - font-size: 25px; - margin-bottom: 12.5px; - font-weight: 500; - line-height: 1.1; -} - -.brand { - font-size: 23px; - line-height: 25px; - color: #fff; - font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; -} - -.banner { - background-color: #175ddc; - height: 56px; -} - -.content { - padding-top: 20px; - padding-bottom: 20px; - padding-left: 15px; - padding-right: 15px; -} - -.footer { - padding: 40px 0 40px 0; - border-top: 1px solid #dee2e6; -} - -/* Bitwarden icons, manually copied */ - -@font-face { - font-family: "bwi-font"; - src: - url(../images/bwi-font.svg) format("svg"), - url(../fonts/bwi-font.ttf) format("truetype"), - url(../fonts/bwi-font.woff) format("woff"), - url(../fonts/bwi-font.woff2) format("woff2"); - font-weight: normal; - font-style: normal; - font-display: block; -} - -.bwi { - /* use !important to prevent issues with browser extensions that change fonts */ - font-family: "bwi-font" !important; - speak: never; - font-style: normal; - font-weight: normal; - font-variant: normal; - text-transform: none; - line-height: 1; - display: inline-block; - /* Better Font Rendering */ - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.bwi-shield:before { - content: "\e932"; -} diff --git a/apps/web/src/app/admin-console/.eslintrc.json b/apps/web/src/app/admin-console/.eslintrc.json new file mode 100644 index 00000000000..d55df3899e7 --- /dev/null +++ b/apps/web/src/app/admin-console/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "../../../../../libs/admin-console/.eslintrc.json" +} diff --git a/apps/web/src/app/admin-console/common/new-base.people.component.ts b/apps/web/src/app/admin-console/common/base-members.component.ts similarity index 96% rename from apps/web/src/app/admin-console/common/new-base.people.component.ts rename to apps/web/src/app/admin-console/common/base-members.component.ts index 90c25e840c0..2d0d66e2930 100644 --- a/apps/web/src/app/admin-console/common/new-base.people.component.ts +++ b/apps/web/src/app/admin-console/common/base-members.component.ts @@ -4,7 +4,6 @@ import { FormControl } from "@angular/forms"; import { firstValueFrom, lastValueFrom, debounceTime, combineLatest, BehaviorSubject } from "rxjs"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; import { @@ -35,7 +34,7 @@ export type UserViewTypes = ProviderUserUserDetailsResponse | OrganizationUserVi * This will replace BasePeopleComponent once all subclasses have been changed over to use this class. */ @Directive() -export abstract class NewBasePeopleComponent { +export abstract class BaseMembersComponent { /** * Shows a banner alerting the admin that users need to be confirmed. */ @@ -52,6 +51,10 @@ export abstract class NewBasePeopleComponent { return this.dataSource.acceptedUserCount > 0; } + get showBulkReinviteUsers(): boolean { + return this.dataSource.invitedUserCount > 0; + } + abstract userType: typeof OrganizationUserType | typeof ProviderUserType; abstract userStatusType: typeof OrganizationUserStatusType | typeof ProviderUserStatusType; @@ -77,7 +80,6 @@ export abstract class NewBasePeopleComponent { protected i18nService: I18nService, protected cryptoService: CryptoService, protected validationService: ValidationService, - protected modalService: ModalService, private logService: LogService, protected userNamePipe: UserNamePipe, protected dialogService: DialogService, @@ -94,7 +96,7 @@ export abstract class NewBasePeopleComponent { abstract edit(user: UserView): void; abstract getUsers(): Promise | UserView[]>; - abstract deleteUser(id: string): Promise; + abstract removeUser(id: string): Promise; abstract reinviteUser(id: string): Promise; abstract confirmUser(user: UserView, publicKey: Uint8Array): Promise; @@ -130,7 +132,7 @@ export abstract class NewBasePeopleComponent { return false; } - this.actionPromise = this.deleteUser(user.id); + this.actionPromise = this.removeUser(user.id); try { await this.actionPromise; this.toastService.showToast({ diff --git a/apps/web/src/app/admin-console/common/base.events.component.ts b/apps/web/src/app/admin-console/common/base.events.component.ts index 12c051271e1..578a9e551de 100644 --- a/apps/web/src/app/admin-console/common/base.events.component.ts +++ b/apps/web/src/app/admin-console/common/base.events.component.ts @@ -8,6 +8,7 @@ import { FileDownloadService } from "@bitwarden/common/platform/abstractions/fil import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; import { EventService } from "../../core"; import { EventExportService } from "../../tools/event-export"; @@ -34,6 +35,7 @@ export abstract class BaseEventsComponent { protected platformUtilsService: PlatformUtilsService, protected logService: LogService, protected fileDownloadService: FileDownloadService, + private toastService: ToastService, ) { const defaultDates = this.eventService.getDefaultDateFilters(); this.start = defaultDates[0]; @@ -164,11 +166,11 @@ export abstract class BaseEventsComponent { try { dates = this.eventService.formatDateFilters(this.start, this.end); } catch (e) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("invalidDateRange"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidDateRange"), + }); return null; } return dates; diff --git a/apps/web/src/app/admin-console/common/base.people.component.ts b/apps/web/src/app/admin-console/common/base.people.component.ts index 0dad7ab7b13..e24f3ac78e7 100644 --- a/apps/web/src/app/admin-console/common/base.people.component.ts +++ b/apps/web/src/app/admin-console/common/base.people.component.ts @@ -22,7 +22,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { OrganizationUserView } from "../organizations/core/views/organization-user.view"; import { UserConfirmComponent } from "../organizations/manage/user-confirm.component"; @@ -127,6 +127,7 @@ export abstract class BasePeopleComponent< protected userNamePipe: UserNamePipe, protected dialogService: DialogService, protected organizationManagementPreferencesService: OrganizationManagementPreferencesService, + protected toastService: ToastService, ) {} abstract edit(user: UserType): void; @@ -251,11 +252,11 @@ export abstract class BasePeopleComponent< this.actionPromise = this.deleteUser(user.id); try { await this.actionPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("removedUserId", this.userNamePipe.transform(user)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("removedUserId", this.userNamePipe.transform(user)), + }); this.removeUser(user); } catch (e) { this.validationService.showError(e); @@ -282,11 +283,11 @@ export abstract class BasePeopleComponent< this.actionPromise = this.revokeUser(user.id); try { await this.actionPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("revokedUserId", this.userNamePipe.transform(user)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("revokedUserId", this.userNamePipe.transform(user)), + }); await this.load(); } catch (e) { this.validationService.showError(e); @@ -298,11 +299,11 @@ export abstract class BasePeopleComponent< this.actionPromise = this.restoreUser(user.id); try { await this.actionPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("restoredUserId", this.userNamePipe.transform(user)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("restoredUserId", this.userNamePipe.transform(user)), + }); await this.load(); } catch (e) { this.validationService.showError(e); @@ -318,11 +319,11 @@ export abstract class BasePeopleComponent< this.actionPromise = this.reinviteUser(user.id); try { await this.actionPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("hasBeenReinvited", this.userNamePipe.transform(user)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("hasBeenReinvited", this.userNamePipe.transform(user)), + }); } catch (e) { this.validationService.showError(e); } @@ -344,11 +345,11 @@ export abstract class BasePeopleComponent< this.actionPromise = this.confirmUser(user, publicKey); await this.actionPromise; updateUser(this); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(user)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(user)), + }); } catch (e) { this.validationService.showError(e); throw e; diff --git a/apps/web/src/app/admin-console/common/people-table-data-source.ts b/apps/web/src/app/admin-console/common/people-table-data-source.ts index db357b4dbcd..5ce7e7bda7d 100644 --- a/apps/web/src/app/admin-console/common/people-table-data-source.ts +++ b/apps/web/src/app/admin-console/common/people-table-data-source.ts @@ -4,7 +4,7 @@ import { } from "@bitwarden/common/admin-console/enums"; import { TableDataSource } from "@bitwarden/components"; -import { StatusType, UserViewTypes } from "./new-base.people.component"; +import { StatusType, UserViewTypes } from "./base-members.component"; const MaxCheckedCount = 500; diff --git a/apps/web/src/app/admin-console/organizations/core/services/user-admin.service.ts b/apps/web/src/app/admin-console/organizations/core/services/user-admin.service.ts index 52a522c89da..9741758e1e0 100644 --- a/apps/web/src/app/admin-console/organizations/core/services/user-admin.service.ts +++ b/apps/web/src/app/admin-console/organizations/core/services/user-admin.service.ts @@ -1,11 +1,11 @@ import { Injectable } from "@angular/core"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { + OrganizationUserApiService, OrganizationUserInviteRequest, OrganizationUserUpdateRequest, -} from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; -import { OrganizationUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; + OrganizationUserDetailsResponse, +} from "@bitwarden/admin-console/common"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CoreOrganizationModule } from "../core-organization.module"; @@ -15,14 +15,14 @@ import { OrganizationUserAdminView } from "../views/organization-user-admin-view export class UserAdminService { constructor( private configService: ConfigService, - private organizationUserService: OrganizationUserService, + private organizationUserApiService: OrganizationUserApiService, ) {} async get( organizationId: string, organizationUserId: string, ): Promise { - const userResponse = await this.organizationUserService.getOrganizationUser( + const userResponse = await this.organizationUserApiService.getOrganizationUser( organizationId, organizationUserId, { @@ -47,7 +47,11 @@ export class UserAdminService { request.groups = user.groups; request.accessSecretsManager = user.accessSecretsManager; - await this.organizationUserService.putOrganizationUser(user.organizationId, user.id, request); + await this.organizationUserApiService.putOrganizationUser( + user.organizationId, + user.id, + request, + ); } async invite(emails: string[], user: OrganizationUserAdminView): Promise { @@ -59,7 +63,7 @@ export class UserAdminService { request.groups = user.groups; request.accessSecretsManager = user.accessSecretsManager; - await this.organizationUserService.postOrganizationUserInvite(user.organizationId, request); + await this.organizationUserApiService.postOrganizationUserInvite(user.organizationId, request); } private async decryptMany( diff --git a/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts b/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts index 86d1f4ded6b..8988f41487c 100644 --- a/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts +++ b/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts @@ -1,4 +1,4 @@ -import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; +import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common"; import { OrganizationUserStatusType, OrganizationUserType, diff --git a/apps/web/src/app/admin-console/organizations/create/organization-information.component.ts b/apps/web/src/app/admin-console/organizations/create/organization-information.component.ts index 602ad82972a..3adfb7340ba 100644 --- a/apps/web/src/app/admin-console/organizations/create/organization-information.component.ts +++ b/apps/web/src/app/admin-console/organizations/create/organization-information.component.ts @@ -19,7 +19,7 @@ export class OrganizationInformationComponent implements OnInit { constructor(private accountService: AccountService) {} async ngOnInit(): Promise { - if (this.formGroup.controls.billingEmail.value) { + if (this.formGroup?.controls?.billingEmail?.value) { return; } diff --git a/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.spec.ts b/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.spec.ts new file mode 100644 index 00000000000..75e63d42428 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.spec.ts @@ -0,0 +1,126 @@ +import { Component } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { provideRouter } from "@angular/router"; +import { RouterTestingHarness } from "@angular/router/testing"; +import { MockProxy, any, mock } from "jest-mock-extended"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { DialogService } from "@bitwarden/components"; + +import { isEnterpriseOrgGuard } from "./is-enterprise-org.guard"; + +@Component({ + template: "

    This is the home screen!

    ", +}) +export class HomescreenComponent {} + +@Component({ + template: "

    This component can only be accessed by a enterprise organization!

    ", +}) +export class IsEnterpriseOrganizationComponent {} + +@Component({ + template: "

    This is the organization upgrade screen!

    ", +}) +export class OrganizationUpgradeScreenComponent {} + +const orgFactory = (props: Partial = {}) => + Object.assign( + new Organization(), + { + id: "myOrgId", + enabled: true, + type: OrganizationUserType.Admin, + }, + props, + ); + +describe("Is Enterprise Org Guard", () => { + let organizationService: MockProxy; + let dialogService: MockProxy; + let routerHarness: RouterTestingHarness; + + beforeEach(async () => { + organizationService = mock(); + dialogService = mock(); + + TestBed.configureTestingModule({ + providers: [ + { provide: OrganizationService, useValue: organizationService }, + { provide: DialogService, useValue: dialogService }, + provideRouter([ + { + path: "", + component: HomescreenComponent, + }, + { + path: "organizations/:organizationId/enterpriseOrgsOnly", + component: IsEnterpriseOrganizationComponent, + canActivate: [isEnterpriseOrgGuard()], + }, + { + path: "organizations/:organizationId/billing/subscription", + component: OrganizationUpgradeScreenComponent, + }, + ]), + ], + }); + + routerHarness = await RouterTestingHarness.create(); + }); + + it("redirects to `/` if the organization id provided is not found", async () => { + const org = orgFactory(); + organizationService.get.calledWith(org.id).mockResolvedValue(null); + await routerHarness.navigateByUrl(`organizations/${org.id}/enterpriseOrgsOnly`); + expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe( + "This is the home screen!", + ); + }); + + it.each([ + ProductTierType.Free, + ProductTierType.Families, + ProductTierType.Teams, + ProductTierType.TeamsStarter, + ])( + "shows a dialog to users of a not enterprise organization and does not proceed with navigation for productTierType '%s'", + async (productTierType) => { + const org = orgFactory({ + type: OrganizationUserType.User, + productTierType: productTierType, + }); + organizationService.get.calledWith(org.id).mockResolvedValue(org); + await routerHarness.navigateByUrl(`organizations/${org.id}/enterpriseOrgsOnly`); + expect(dialogService.openSimpleDialog).toHaveBeenCalled(); + expect( + routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "", + ).not.toBe("This component can only be accessed by a enterprise organization!"); + }, + ); + + it("redirects users with billing access to the billing screen to upgrade", async () => { + const org = orgFactory({ + type: OrganizationUserType.Owner, + productTierType: ProductTierType.Teams, + }); + organizationService.get.calledWith(org.id).mockResolvedValue(org); + dialogService.openSimpleDialog.calledWith(any()).mockResolvedValue(true); + await routerHarness.navigateByUrl(`organizations/${org.id}/enterpriseOrgsOnly`); + expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe( + "This is the organization upgrade screen!", + ); + }); + + it("proceeds with navigation if the organization in question is a enterprise organization", async () => { + const org = orgFactory({ productTierType: ProductTierType.Enterprise }); + organizationService.get.calledWith(org.id).mockResolvedValue(org); + await routerHarness.navigateByUrl(`organizations/${org.id}/enterpriseOrgsOnly`); + expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe( + "This component can only be accessed by a enterprise organization!", + ); + }); +}); diff --git a/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.ts b/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.ts index 8e35c60db96..3373f0cfd53 100644 --- a/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.ts +++ b/apps/web/src/app/admin-console/organizations/guards/is-enterprise-org.guard.ts @@ -1,44 +1,45 @@ -import { Injectable } from "@angular/core"; -import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router"; -import { firstValueFrom } from "rxjs"; +import { inject } from "@angular/core"; +import { + ActivatedRouteSnapshot, + CanActivateFn, + Router, + RouterStateSnapshot, +} from "@angular/router"; +import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; 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 { DialogService } from "@bitwarden/components"; -@Injectable({ - providedIn: "root", -}) -export class IsEnterpriseOrgGuard implements CanActivate { - constructor( - private router: Router, - private organizationService: OrganizationService, - private dialogService: DialogService, - private configService: ConfigService, - ) {} +/** + * `CanActivateFn` that checks if the organization matching the id in the URL + * parameters is of enterprise type. If the organization is not enterprise instructions are + * provided on how to upgrade into an enterprise organization, and the user is redirected + * if they have access to upgrade the organization. If the organization is + * enterprise routing proceeds." + */ +export function isEnterpriseOrgGuard(): CanActivateFn { + return async (route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => { + const router = inject(Router); + const organizationService = inject(OrganizationService); + const dialogService = inject(DialogService); - async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { - const isMemberAccessReportEnabled = await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.MemberAccessReport), - ); - - // TODO: Remove on "MemberAccessReport" feature flag cleanup - if (!isMemberAccessReportEnabled) { - return this.router.createUrlTree(["/"]); - } - - const org = await this.organizationService.get(route.params.organizationId); + const org = await organizationService.get(route.params.organizationId); if (org == null) { - return this.router.createUrlTree(["/"]); + return router.createUrlTree(["/"]); + } + + // TODO: Remove on "MemberAccessReport" feature flag cleanup + if (!canAccessFeature(FeatureFlag.MemberAccessReport)) { + return router.createUrlTree(["/"]); } if (org.productTierType != ProductTierType.Enterprise) { // Users without billing permission can't access billing if (!org.canEditSubscription) { - await this.dialogService.openSimpleDialog({ + await dialogService.openSimpleDialog({ title: { key: "upgradeOrganizationEnterprise" }, content: { key: "onlyAvailableForEnterpriseOrganization" }, acceptButtonText: { key: "ok" }, @@ -47,7 +48,7 @@ export class IsEnterpriseOrgGuard implements CanActivate { }); return false; } else { - const upgradeConfirmed = await this.dialogService.openSimpleDialog({ + const upgradeConfirmed = await dialogService.openSimpleDialog({ title: { key: "upgradeOrganizationEnterprise" }, content: { key: "onlyAvailableForEnterpriseOrganization" }, acceptButtonText: { key: "upgradeOrganization" }, @@ -55,13 +56,13 @@ export class IsEnterpriseOrgGuard implements CanActivate { icon: "bwi-arrow-circle-up", }); if (upgradeConfirmed) { - await this.router.navigate(["organizations", org.id, "billing", "subscription"], { - queryParams: { upgrade: true }, + await router.navigate(["organizations", org.id, "billing", "subscription"], { + queryParams: { upgrade: true, productTierType: ProductTierType.Enterprise }, }); } } } return org.productTierType == ProductTierType.Enterprise; - } + }; } diff --git a/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.spec.ts b/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.spec.ts index cf9a7b31dc6..653651bf69a 100644 --- a/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.spec.ts +++ b/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.spec.ts @@ -24,7 +24,7 @@ export class PaidOrganizationOnlyComponent {} @Component({ template: "

    This is the organization upgrade screen!

    ", }) -export class OrganizationUpgradeScreen {} +export class OrganizationUpgradeScreenComponent {} const orgFactory = (props: Partial = {}) => Object.assign( @@ -62,7 +62,7 @@ describe("Is Paid Org Guard", () => { }, { path: "organizations/:organizationId/billing/subscription", - component: OrganizationUpgradeScreen, + component: OrganizationUpgradeScreenComponent, }, ]), ], diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index 563905548df..336285b2a45 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -1,5 +1,5 @@ - - + + @@ -103,12 +103,7 @@ *ngIf="organization.canManageScim" > - - - - - - + {{ "accessingUsingProvider" | i18n: organization.providerName }} - - + diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts index 4383656bee1..65fefe01a3b 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts @@ -22,12 +22,10 @@ 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { BannerModule, IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components"; +import { BannerModule, IconModule } from "@bitwarden/components"; -import { PaymentMethodWarningsModule } from "../../../billing/shared"; import { OrgSwitcherComponent } from "../../../layouts/org-switcher/org-switcher.component"; -import { ProductSwitcherModule } from "../../../layouts/product-switcher/product-switcher.module"; -import { ToggleWidthComponent } from "../../../layouts/toggle-width.component"; +import { WebLayoutModule } from "../../../layouts/web-layout.module"; import { AdminConsoleLogo } from "../../icons/admin-console-logo"; @Component({ @@ -38,14 +36,10 @@ import { AdminConsoleLogo } from "../../icons/admin-console-logo"; CommonModule, RouterModule, JslibModule, - LayoutComponent, + WebLayoutModule, IconModule, - NavigationModule, OrgSwitcherComponent, BannerModule, - PaymentMethodWarningsModule, - ToggleWidthComponent, - ProductSwitcherModule, ], }) export class OrganizationLayoutComponent implements OnInit, OnDestroy { @@ -64,10 +58,6 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { FeatureFlag.EnableConsolidatedBilling, ); - protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$( - FeatureFlag.ShowPaymentMethodWarningBanners, - ); - constructor( private route: ActivatedRoute, private organizationService: OrganizationService, diff --git a/apps/web/src/app/admin-console/organizations/manage/entity-events.component.html b/apps/web/src/app/admin-console/organizations/manage/entity-events.component.html index d296514e357..68582e8b2ea 100644 --- a/apps/web/src/app/admin-console/organizations/manage/entity-events.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/entity-events.component.html @@ -6,31 +6,27 @@
    -
    - - - - -
    + + {{ "from" | i18n }} + + - -
    - - - - -
    + + {{ "to" | i18n }} + +
    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..36489e0ab1d 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 @@ -14,19 +14,17 @@ import { takeUntil, } from "rxjs"; +import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { 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"; import { UserId } from "@bitwarden/common/types/guid"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { CollectionAdminService } from "../../../vault/core/collection-admin.service"; import { CollectionAdminView } from "../../../vault/core/views/collection-admin.view"; @@ -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; @@ -136,7 +131,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { ); private get orgMembers$(): Observable> { - return from(this.organizationUserService.getAllUsers(this.organizationId)).pipe( + return from(this.organizationUserApiService.getAllUsers(this.organizationId)).pipe( map((response) => response.data.map((m) => ({ id: m.id, @@ -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), @@ -215,7 +202,7 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { @Inject(DIALOG_DATA) private params: GroupAddEditDialogParams, private dialogRef: DialogRef, private apiService: ApiService, - private organizationUserService: OrganizationUserService, + private organizationUserApiService: OrganizationUserApiService, private groupService: GroupService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, @@ -224,9 +211,9 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { private changeDetectorRef: ChangeDetectorRef, private dialogService: DialogService, private organizationService: OrganizationService, - private configService: ConfigService, private accountService: AccountService, private collectionAdminService: CollectionAdminService, + private toastService: ToastService, ) { this.tabIndex = params.initialTab ?? GroupAddEditTabType.Info; } @@ -242,27 +229,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 @@ -308,11 +281,14 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { if (this.groupForm.invalid) { if (this.tabIndex !== GroupAddEditTabType.Info) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("groupInfo")), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t( + "fieldOnTabRequiresAttention", + this.i18nService.t("groupInfo"), + ), + }); } return; } @@ -328,11 +304,14 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { await this.groupService.save(groupView); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t(this.editMode ? "editedGroupId" : "createdGroupId", formValue.name), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t( + this.editMode ? "editedGroupId" : "createdGroupId", + formValue.name, + ), + }); this.dialogRef.close(GroupAddEditDialogResultType.Saved); }; @@ -353,11 +332,11 @@ export class GroupAddEditComponent implements OnInit, OnDestroy { await this.groupService.delete(this.organizationId, this.groupId); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("deletedGroupId", this.group.name), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedGroupId", this.group.name), + }); this.dialogRef.close(GroupAddEditDialogResultType.Deleted); }; } @@ -384,7 +363,6 @@ function mapToAccessSelections(group: GroupView, items: AccessItemView[]): Acces function mapToAccessItemViews( collections: CollectionAdminView[], organization: Organization, - flexibleCollectionsV1Enabled: boolean, group?: GroupView, ): AccessItemView[] { return ( @@ -396,7 +374,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/manage/groups.component.html b/apps/web/src/app/admin-console/organizations/manage/groups.component.html index 1a1a7cdb904..1254d48cc76 100644 --- a/apps/web/src/app/admin-console/organizations/manage/groups.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/groups.component.html @@ -1,7 +1,7 @@ + +

    {{ "noGroupsInList" | i18n }}

    + + + + + + + + + + {{ "name" | i18n }} + {{ "collections" | i18n }} + + - - + + + + + + + + + + + - - - -
    - - - - - - - - - - - - - + + + + + + - - - - - - - - - - + + + + + + + + + + + - diff --git a/apps/web/src/app/admin-console/organizations/manage/groups.component.ts b/apps/web/src/app/admin-console/organizations/manage/groups.component.ts index 7c86ac28498..dfb6f349ebd 100644 --- a/apps/web/src/app/admin-console/organizations/manage/groups.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/groups.component.ts @@ -1,4 +1,6 @@ -import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormControl } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; import { BehaviorSubject, @@ -7,21 +9,15 @@ import { from, lastValueFrom, map, - Subject, switchMap, - takeUntil, tap, } from "rxjs"; -import { first } from "rxjs/operators"; +import { debounceTime, first } from "rxjs/operators"; -import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data"; import { Collection } from "@bitwarden/common/vault/models/domain/collection"; @@ -30,7 +26,7 @@ import { CollectionResponse, } from "@bitwarden/common/vault/models/response/collection.response"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, TableDataSource, ToastService } from "@bitwarden/components"; import { InternalGroupService as GroupService, GroupView } from "../core"; @@ -40,21 +36,7 @@ import { openGroupAddEditDialog, } from "./group-add-edit.component"; -type CollectionViewMap = { - [id: string]: CollectionView; -}; - type GroupDetailsRow = { - /** - * Group Id (used for searching) - */ - id: string; - - /** - * Group name (used for searching) - */ - name: string; - /** * Details used for displaying group information */ @@ -72,59 +54,38 @@ type GroupDetailsRow = { }; /** - * @deprecated To be replaced with NewGroupsComponent which significantly refactors this component. - * The GroupsComponentRefactor flag switches between the old and new components; this component will be removed when - * the feature flag is removed. + * Custom filter predicate that filters the groups table by id and name only. + * This is required because the default implementation searches by all properties, which can unintentionally match + * with members' names (who are assigned to the group) or collection names (which the group has access to). */ +const groupsFilter = (filter: string) => { + const transformedFilter = filter.trim().toLowerCase(); + return (data: GroupDetailsRow) => { + const group = data.details; + + return ( + group.id.toLowerCase().indexOf(transformedFilter) != -1 || + group.name.toLowerCase().indexOf(transformedFilter) != -1 + ); + }; +}; + @Component({ - selector: "app-org-groups", templateUrl: "groups.component.html", }) -export class GroupsComponent implements OnInit, OnDestroy { - @ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef; - @ViewChild("usersTemplate", { read: ViewContainerRef, static: true }) - usersModalRef: ViewContainerRef; - +export class GroupsComponent { loading = true; organizationId: string; - groups: GroupDetailsRow[]; - protected didScroll = false; - protected pageSize = 100; + protected dataSource = new TableDataSource(); + protected searchControl = new FormControl(""); + + // Fixed sizes used for cdkVirtualScroll + protected rowHeight = 46; + protected rowHeightClass = `tw-h-[46px]`; + protected ModalTabType = GroupAddEditTabType; - - private pagedGroupsCount = 0; - private pagedGroups: GroupDetailsRow[]; - private searchedGroups: GroupDetailsRow[]; - private _searchText$ = new BehaviorSubject(""); - private destroy$ = new Subject(); private refreshGroups$ = new BehaviorSubject(null); - private isSearching: boolean = false; - - get searchText() { - return this._searchText$.value; - } - set searchText(value: string) { - this._searchText$.next(value); - // Manually update as we are not using the search pipe in the template - this.updateSearchedGroups(); - } - - /** - * The list of groups that should be visible in the table. - * This is needed as there are two modes (paging/searching) and - * we need a reference to the currently visible groups for - * the Select All checkbox - */ - get visibleGroups(): GroupDetailsRow[] { - if (this.isPaging()) { - return this.pagedGroups; - } - if (this.isSearching) { - return this.searchedGroups; - } - return this.groups; - } constructor( private apiService: ApiService, @@ -132,14 +93,10 @@ export class GroupsComponent implements OnInit, OnDestroy { private route: ActivatedRoute, private i18nService: I18nService, private dialogService: DialogService, - private platformUtilsService: PlatformUtilsService, - private searchService: SearchService, private logService: LogService, private collectionService: CollectionService, - private searchPipe: SearchPipe, - ) {} - - async ngOnInit() { + private toastService: ToastService, + ) { this.route.params .pipe( tap((params) => (this.organizationId = params.organizationId)), @@ -156,68 +113,31 @@ export class GroupsComponent implements OnInit, OnDestroy { ]), ), map(([collectionMap, groups]) => { - return groups - .sort(Utils.getSortFunction(this.i18nService, "name")) - .map((g) => ({ - id: g.id, - name: g.name, - details: g, - checked: false, - collectionNames: g.collections - .map((c) => collectionMap[c.id]?.name) - .sort(this.i18nService.collator?.compare), - })); + return groups.map((g) => ({ + id: g.id, + name: g.name, + details: g, + checked: false, + collectionNames: g.collections + .map((c) => collectionMap[c.id]?.name) + .sort(this.i18nService.collator?.compare), + })); }), - takeUntil(this.destroy$), + takeUntilDestroyed(), ) .subscribe((groups) => { - this.groups = groups; - this.resetPaging(); - this.updateSearchedGroups(); + this.dataSource.data = groups; this.loading = false; }); - this.route.queryParams - .pipe( - first(), - concatMap(async (qParams) => { - this.searchText = qParams.search; - }), - takeUntil(this.destroy$), - ) - .subscribe(); + // Connect the search input to the table dataSource filter input + this.searchControl.valueChanges + .pipe(debounceTime(200), takeUntilDestroyed()) + .subscribe((v) => (this.dataSource.filter = groupsFilter(v))); - this._searchText$ - .pipe( - switchMap((searchText) => this.searchService.isSearchable(searchText)), - takeUntil(this.destroy$), - ) - .subscribe((isSearchable) => { - this.isSearching = isSearchable; - }); - } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } - - loadMore() { - if (!this.groups || this.groups.length <= this.pageSize) { - return; - } - const pagedLength = this.pagedGroups.length; - let pagedSize = this.pageSize; - if (pagedLength === 0 && this.pagedGroupsCount > this.pageSize) { - pagedSize = this.pagedGroupsCount; - } - if (this.groups.length > pagedLength) { - this.pagedGroups = this.pagedGroups.concat( - this.groups.slice(pagedLength, pagedLength + pagedSize), - ); - } - this.pagedGroupsCount = this.pagedGroups.length; - this.didScroll = this.pagedGroups.length > this.pageSize; + this.route.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((qParams) => { + this.searchControl.setValue(qParams.search); + }); } async edit( @@ -237,14 +157,12 @@ export class GroupsComponent implements OnInit, OnDestroy { if (result == GroupAddEditDialogResultType.Saved) { this.refreshGroups$.next(); } else if (result == GroupAddEditDialogResultType.Deleted) { - this.removeGroup(group.details.id); + this.removeGroup(group); } } - add() { - // 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.edit(null); + async add() { + await this.edit(null); } async delete(groupRow: GroupDetailsRow) { @@ -259,19 +177,19 @@ export class GroupsComponent implements OnInit, OnDestroy { try { await this.groupService.delete(this.organizationId, groupRow.details.id); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("deletedGroupId", groupRow.details.name), - ); - this.removeGroup(groupRow.details.id); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedGroupId", groupRow.details.name), + }); + this.removeGroup(groupRow); } catch (e) { this.logService.error(e); } } async deleteAllSelected() { - const groupsToDelete = this.groups.filter((g) => g.checked); + const groupsToDelete = this.dataSource.data.filter((g) => g.checked); if (groupsToDelete.length == 0) { return; @@ -295,46 +213,31 @@ export class GroupsComponent implements OnInit, OnDestroy { this.organizationId, groupsToDelete.map((g) => g.details.id), ); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("deletedManyGroups", groupsToDelete.length.toString()), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedManyGroups", groupsToDelete.length.toString()), + }); - groupsToDelete.forEach((g) => this.removeGroup(g.details.id)); + groupsToDelete.forEach((g) => this.removeGroup(g)); } catch (e) { this.logService.error(e); } } - resetPaging() { - this.pagedGroups = []; - this.loadMore(); - } - check(groupRow: GroupDetailsRow) { groupRow.checked = !groupRow.checked; } toggleAllVisible(event: Event) { - this.visibleGroups.forEach((g) => (g.checked = (event.target as HTMLInputElement).checked)); + this.dataSource.filteredData.forEach( + (g) => (g.checked = (event.target as HTMLInputElement).checked), + ); } - isPaging() { - const searching = this.isSearching; - if (searching && this.didScroll) { - this.resetPaging(); - } - return !searching && this.groups && this.groups.length > this.pageSize; - } - - private removeGroup(id: string) { - const index = this.groups.findIndex((g) => g.details.id === id); - if (index > -1) { - this.groups.splice(index, 1); - this.resetPaging(); - this.updateSearchedGroups(); - } + private removeGroup(groupRow: GroupDetailsRow) { + // Assign a new array to dataSource.data to trigger the setters and update the table + this.dataSource.data = this.dataSource.data.filter((g) => g !== groupRow); } private async toCollectionMap(response: ListResponse) { @@ -344,21 +247,9 @@ export class GroupsComponent implements OnInit, OnDestroy { const decryptedCollections = await this.collectionService.decryptMany(collections); // Convert to an object using collection Ids as keys for faster name lookups - const collectionMap: CollectionViewMap = {}; + const collectionMap: Record = {}; decryptedCollections.forEach((c) => (collectionMap[c.id] = c)); return collectionMap; } - - private updateSearchedGroups() { - if (this.isSearching) { - // Making use of the pipe in the component as we need know which groups where filtered - this.searchedGroups = this.searchPipe.transform( - this.groups, - this.searchText, - (group) => group.details.name, - (group) => group.details.id, - ); - } - } } diff --git a/apps/web/src/app/admin-console/organizations/manage/new-groups.component.html b/apps/web/src/app/admin-console/organizations/manage/new-groups.component.html deleted file mode 100644 index 3e659e5b6a8..00000000000 --- a/apps/web/src/app/admin-console/organizations/manage/new-groups.component.html +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - - - {{ "loading" | i18n }} - - -

    {{ "noGroupsInList" | i18n }}

    - - - - - - - - - - {{ "name" | i18n }} - {{ "collections" | i18n }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    diff --git a/apps/web/src/app/admin-console/organizations/manage/new-groups.component.ts b/apps/web/src/app/admin-console/organizations/manage/new-groups.component.ts deleted file mode 100644 index e5e99333e64..00000000000 --- a/apps/web/src/app/admin-console/organizations/manage/new-groups.component.ts +++ /dev/null @@ -1,255 +0,0 @@ -import { Component } from "@angular/core"; -import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { FormControl } from "@angular/forms"; -import { ActivatedRoute } from "@angular/router"; -import { - BehaviorSubject, - combineLatest, - concatMap, - from, - lastValueFrom, - map, - switchMap, - tap, -} from "rxjs"; -import { debounceTime, first } from "rxjs/operators"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { ListResponse } from "@bitwarden/common/models/response/list.response"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; -import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data"; -import { Collection } from "@bitwarden/common/vault/models/domain/collection"; -import { - CollectionDetailsResponse, - CollectionResponse, -} from "@bitwarden/common/vault/models/response/collection.response"; -import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; -import { DialogService, TableDataSource, ToastService } from "@bitwarden/components"; - -import { InternalGroupService as GroupService, GroupView } from "../core"; - -import { - GroupAddEditDialogResultType, - GroupAddEditTabType, - openGroupAddEditDialog, -} from "./group-add-edit.component"; - -type GroupDetailsRow = { - /** - * Details used for displaying group information - */ - details: GroupView; - - /** - * True if the group is selected in the table - */ - checked?: boolean; - - /** - * A list of collection names the group has access to - */ - collectionNames?: string[]; -}; - -/** - * Custom filter predicate that filters the groups table by id and name only. - * This is required because the default implementation searches by all properties, which can unintentionally match - * with members' names (who are assigned to the group) or collection names (which the group has access to). - */ -const groupsFilter = (filter: string) => { - const transformedFilter = filter.trim().toLowerCase(); - return (data: GroupDetailsRow) => { - const group = data.details; - - return ( - group.id.toLowerCase().indexOf(transformedFilter) != -1 || - group.name.toLowerCase().indexOf(transformedFilter) != -1 - ); - }; -}; - -@Component({ - templateUrl: "new-groups.component.html", -}) -export class NewGroupsComponent { - loading = true; - organizationId: string; - - protected dataSource = new TableDataSource(); - protected searchControl = new FormControl(""); - - // Fixed sizes used for cdkVirtualScroll - protected rowHeight = 46; - protected rowHeightClass = `tw-h-[46px]`; - - protected ModalTabType = GroupAddEditTabType; - private refreshGroups$ = new BehaviorSubject(null); - - constructor( - private apiService: ApiService, - private groupService: GroupService, - private route: ActivatedRoute, - private i18nService: I18nService, - private dialogService: DialogService, - private logService: LogService, - private collectionService: CollectionService, - private toastService: ToastService, - ) { - this.route.params - .pipe( - tap((params) => (this.organizationId = params.organizationId)), - switchMap(() => - combineLatest([ - // collectionMap - from(this.apiService.getCollections(this.organizationId)).pipe( - concatMap((response) => this.toCollectionMap(response)), - ), - // groups - this.refreshGroups$.pipe( - switchMap(() => this.groupService.getAll(this.organizationId)), - ), - ]), - ), - map(([collectionMap, groups]) => { - return groups.map((g) => ({ - id: g.id, - name: g.name, - details: g, - checked: false, - collectionNames: g.collections - .map((c) => collectionMap[c.id]?.name) - .sort(this.i18nService.collator?.compare), - })); - }), - takeUntilDestroyed(), - ) - .subscribe((groups) => { - this.dataSource.data = groups; - this.loading = false; - }); - - // Connect the search input to the table dataSource filter input - this.searchControl.valueChanges - .pipe(debounceTime(200), takeUntilDestroyed()) - .subscribe((v) => (this.dataSource.filter = groupsFilter(v))); - - this.route.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((qParams) => { - this.searchControl.setValue(qParams.search); - }); - } - - async edit( - group: GroupDetailsRow, - startingTabIndex: GroupAddEditTabType = GroupAddEditTabType.Info, - ) { - const dialogRef = openGroupAddEditDialog(this.dialogService, { - data: { - initialTab: startingTabIndex, - organizationId: this.organizationId, - groupId: group != null ? group.details.id : null, - }, - }); - - const result = await lastValueFrom(dialogRef.closed); - - if (result == GroupAddEditDialogResultType.Saved) { - this.refreshGroups$.next(); - } else if (result == GroupAddEditDialogResultType.Deleted) { - this.removeGroup(group); - } - } - - async add() { - await this.edit(null); - } - - async delete(groupRow: GroupDetailsRow) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: groupRow.details.name, - content: { key: "deleteGroupConfirmation" }, - type: "warning", - }); - if (!confirmed) { - return false; - } - - try { - await this.groupService.delete(this.organizationId, groupRow.details.id); - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("deletedGroupId", groupRow.details.name), - }); - this.removeGroup(groupRow); - } catch (e) { - this.logService.error(e); - } - } - - async deleteAllSelected() { - const groupsToDelete = this.dataSource.data.filter((g) => g.checked); - - if (groupsToDelete.length == 0) { - return; - } - - const deleteMessage = groupsToDelete.map((g) => g.details.name).join(", "); - const confirmed = await this.dialogService.openSimpleDialog({ - title: { - key: "deleteMultipleGroupsConfirmation", - placeholders: [groupsToDelete.length.toString()], - }, - content: deleteMessage, - type: "warning", - }); - if (!confirmed) { - return false; - } - - try { - await this.groupService.deleteMany( - this.organizationId, - groupsToDelete.map((g) => g.details.id), - ); - this.toastService.showToast({ - variant: "success", - title: null, - message: this.i18nService.t("deletedManyGroups", groupsToDelete.length.toString()), - }); - - groupsToDelete.forEach((g) => this.removeGroup(g)); - } catch (e) { - this.logService.error(e); - } - } - - check(groupRow: GroupDetailsRow) { - groupRow.checked = !groupRow.checked; - } - - toggleAllVisible(event: Event) { - this.dataSource.filteredData.forEach( - (g) => (g.checked = (event.target as HTMLInputElement).checked), - ); - } - - private removeGroup(groupRow: GroupDetailsRow) { - // Assign a new array to dataSource.data to trigger the setters and update the table - this.dataSource.data = this.dataSource.data.filter((g) => g !== groupRow); - } - - private async toCollectionMap(response: ListResponse) { - const collections = response.data.map( - (r) => new Collection(new CollectionData(r as CollectionDetailsResponse)), - ); - const decryptedCollections = await this.collectionService.decryptMany(collections); - - // Convert to an object using collection Ids as keys for faster name lookups - const collectionMap: Record = {}; - decryptedCollections.forEach((c) => (collectionMap[c.id] = c)); - - return collectionMap; - } -} diff --git a/apps/web/src/app/admin-console/organizations/manage/verify-recover-delete-org.component.ts b/apps/web/src/app/admin-console/organizations/manage/verify-recover-delete-org.component.ts index 0039347dc61..10969350695 100644 --- a/apps/web/src/app/admin-console/organizations/manage/verify-recover-delete-org.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/verify-recover-delete-org.component.ts @@ -6,6 +6,7 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { OrganizationVerifyDeleteRecoverRequest } from "@bitwarden/common/admin-console/models/request/organization-verify-delete-recover.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; import { SharedModule } from "../../../shared/shared.module"; @@ -27,6 +28,7 @@ export class VerifyRecoverDeleteOrgComponent implements OnInit { private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private route: ActivatedRoute, + private toastService: ToastService, ) {} async ngOnInit() { @@ -44,11 +46,11 @@ export class VerifyRecoverDeleteOrgComponent implements OnInit { submit = async () => { const request = new OrganizationVerifyDeleteRecoverRequest(this.token); await this.apiService.deleteUsingToken(this.orgId, request); - this.platformUtilsService.showToast( - "success", - this.i18nService.t("organizationDeleted"), - this.i18nService.t("organizationDeletedDesc"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("organizationDeleted"), + message: this.i18nService.t("organizationDeletedDesc"), + }); await this.router.navigate(["/"]); }; } diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component.ts new file mode 100644 index 00000000000..5a5da935a66 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component.ts @@ -0,0 +1,99 @@ +import { Directive, OnInit } from "@angular/core"; + +import { + OrganizationUserBulkPublicKeyResponse, + OrganizationUserBulkResponse, +} from "@bitwarden/admin-console/common"; +import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk-public-key.response"; +import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; + +import { BulkUserDetails } from "./bulk-status.component"; + +@Directive() +export abstract class BaseBulkConfirmComponent implements OnInit { + protected users: BulkUserDetails[]; + + protected excludedUsers: BulkUserDetails[]; + protected filteredUsers: BulkUserDetails[]; + + protected publicKeys: Map = new Map(); + protected fingerprints: Map = new Map(); + protected statuses: Map = new Map(); + + protected done = false; + protected loading = true; + protected error: string; + + protected constructor( + protected cryptoService: CryptoService, + protected i18nService: I18nService, + ) {} + + async ngOnInit() { + this.excludedUsers = this.users.filter((user) => !this.isAccepted(user)); + this.filteredUsers = this.users.filter((user) => this.isAccepted(user)); + + if (this.filteredUsers.length <= 0) { + this.done = true; + } + + const publicKeysResponse = await this.getPublicKeys(); + + for (const entry of publicKeysResponse.data) { + const publicKey = Utils.fromB64ToArray(entry.key); + const fingerprint = await this.cryptoService.getFingerprint(entry.userId, publicKey); + if (fingerprint != null) { + this.publicKeys.set(entry.id, publicKey); + this.fingerprints.set(entry.id, fingerprint.join("-")); + } + } + + this.loading = false; + } + + submit = async () => { + this.loading = true; + try { + const key = await this.getCryptoKey(); + const userIdsWithKeys: { id: string; key: string }[] = []; + + for (const user of this.filteredUsers) { + const publicKey = this.publicKeys.get(user.id); + if (publicKey == null) { + continue; + } + const encryptedKey = await this.cryptoService.rsaEncrypt(key.key, publicKey); + userIdsWithKeys.push({ + id: user.id, + key: encryptedKey.encryptedString, + }); + } + + const userBulkResponse = await this.postConfirmRequest(userIdsWithKeys); + + userBulkResponse.data.forEach((entry) => { + const error = entry.error !== "" ? entry.error : this.i18nService.t("bulkConfirmMessage"); + this.statuses.set(entry.id, error); + }); + + this.done = true; + } catch (e) { + this.error = e.message; + } + this.loading = false; + }; + + protected abstract getCryptoKey(): Promise; + protected abstract getPublicKeys(): Promise< + ListResponse + >; + protected abstract isAccepted(user: BulkUserDetails): boolean; + protected abstract postConfirmRequest( + userIdsWithKeys: { id: string; key: string }[], + ): Promise>; +} diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-remove.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-remove.component.ts new file mode 100644 index 00000000000..80514e85995 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/base-bulk-remove.component.ts @@ -0,0 +1,40 @@ +import { Directive } from "@angular/core"; + +import { OrganizationUserBulkResponse } from "@bitwarden/admin-console/common"; +import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +@Directive() +export abstract class BaseBulkRemoveComponent { + protected showNoMasterPasswordWarning: boolean; + protected statuses: Map = new Map(); + + protected done = false; + protected loading = false; + protected error: string; + + protected constructor(protected i18nService: I18nService) {} + + submit = async () => { + this.loading = true; + try { + const deleteUsersResponse = await this.deleteUsers(); + deleteUsersResponse.data.forEach((entry) => { + const error = entry.error !== "" ? entry.error : this.i18nService.t("bulkRemovedMessage"); + this.statuses.set(entry.id, error); + }); + this.done = true; + } catch (e) { + this.error = e.message; + } + + this.loading = false; + }; + + protected abstract deleteUsers(): Promise< + ListResponse + >; + + protected abstract get removeUsersWarning(): string; +} diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm.component.ts index d94edd55f85..14653169338 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-confirm.component.ts @@ -1,9 +1,11 @@ import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog"; import { Component, Inject, OnInit } from "@angular/core"; +import { + OrganizationUserApiService, + OrganizationUserBulkConfirmRequest, +} from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; -import { OrganizationUserBulkConfirmRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -40,7 +42,7 @@ export class BulkConfirmComponent implements OnInit { @Inject(DIALOG_DATA) protected data: BulkConfirmDialogData, protected cryptoService: CryptoService, protected apiService: ApiService, - private organizationUserService: OrganizationUserService, + private organizationUserApiService: OrganizationUserApiService, private i18nService: I18nService, ) { this.organizationId = data.organizationId; @@ -104,7 +106,7 @@ export class BulkConfirmComponent implements OnInit { } protected async getPublicKeys() { - return await this.organizationUserService.postOrganizationUsersPublicKey( + return await this.organizationUserApiService.postOrganizationUsersPublicKey( this.organizationId, this.filteredUsers.map((user) => user.id), ); @@ -116,7 +118,7 @@ export class BulkConfirmComponent implements OnInit { protected async postConfirmRequest(userIdsWithKeys: any[]) { const request = new OrganizationUserBulkConfirmRequest(userIdsWithKeys); - return await this.organizationUserService.postOrganizationUserBulkConfirm( + return await this.organizationUserApiService.postOrganizationUserBulkConfirm( this.organizationId, request, ); diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-enable-sm-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-enable-sm-dialog.component.ts index 95dd180f45b..4b7b41a5c8c 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-enable-sm-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-enable-sm-dialog.component.ts @@ -1,10 +1,10 @@ import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; import { Component, Inject, OnInit } from "@angular/core"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; +import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService, TableDataSource } from "@bitwarden/components"; +import { DialogService, TableDataSource, ToastService } from "@bitwarden/components"; import { OrganizationUserView } from "../../../core"; @@ -21,9 +21,10 @@ export class BulkEnableSecretsManagerDialogComponent implements OnInit { constructor( public dialogRef: DialogRef, @Inject(DIALOG_DATA) private data: BulkEnableSecretsManagerDialogData, - private organizationUserService: OrganizationUserService, + private organizationUserApiService: OrganizationUserApiService, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, + private toastService: ToastService, ) {} ngOnInit(): void { @@ -31,15 +32,15 @@ export class BulkEnableSecretsManagerDialogComponent implements OnInit { } submit = async () => { - await this.organizationUserService.putOrganizationUserBulkEnableSecretsManager( + await this.organizationUserApiService.putOrganizationUserBulkEnableSecretsManager( this.data.orgId, this.dataSource.data.map((u) => u.id), ); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("activatedAccessToSecretsManager"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("activatedAccessToSecretsManager"), + }); this.dialogRef.close(); }; diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove.component.ts index 15a24eb25eb..74939238fcc 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-remove.component.ts @@ -1,8 +1,8 @@ import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog"; import { Component, Inject } from "@angular/core"; +import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DialogService } from "@bitwarden/components"; @@ -33,7 +33,7 @@ export class BulkRemoveComponent { @Inject(DIALOG_DATA) protected data: BulkRemoveDialogData, protected apiService: ApiService, protected i18nService: I18nService, - private organizationUserService: OrganizationUserService, + private organizationUserApiService: OrganizationUserApiService, ) { this.organizationId = data.organizationId; this.users = data.users; @@ -45,7 +45,7 @@ export class BulkRemoveComponent { submit = async () => { this.loading = true; try { - const response = await this.deleteUsers(); + const response = await this.removeUsers(); response.data.forEach((entry) => { const error = entry.error !== "" ? entry.error : this.i18nService.t("bulkRemovedMessage"); @@ -59,8 +59,8 @@ export class BulkRemoveComponent { this.loading = false; }; - protected async deleteUsers() { - return await this.organizationUserService.deleteManyOrganizationUsers( + protected async removeUsers() { + return await this.organizationUserApiService.removeManyOrganizationUsers( this.organizationId, this.users.map((user) => user.id), ); diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts index a2ab93dd0e1..0ac413eb820 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.ts @@ -1,7 +1,7 @@ import { DIALOG_DATA } from "@angular/cdk/dialog"; import { Component, Inject } from "@angular/core"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; +import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DialogService } from "@bitwarden/components"; @@ -32,7 +32,7 @@ export class BulkRestoreRevokeComponent { constructor( protected i18nService: I18nService, - private organizationUserService: OrganizationUserService, + private organizationUserApiService: OrganizationUserApiService, @Inject(DIALOG_DATA) protected data: BulkRestoreDialogParams, ) { this.isRevoking = data.isRevoking; @@ -66,12 +66,12 @@ export class BulkRestoreRevokeComponent { protected async performBulkUserAction() { const userIds = this.users.map((user) => user.id); if (this.isRevoking) { - return await this.organizationUserService.revokeManyOrganizationUsers( + return await this.organizationUserApiService.revokeManyOrganizationUsers( this.organizationId, userIds, ); } else { - return await this.organizationUserService.restoreManyOrganizationUsers( + return await this.organizationUserApiService.restoreManyOrganizationUsers( this.organizationId, userIds, ); diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts index ffaf27ea46d..7bcae82cfd8 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-status.component.ts @@ -1,7 +1,7 @@ import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog"; import { Component, Inject, OnInit } from "@angular/core"; -import { OrganizationUserBulkResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; +import { OrganizationUserBulkResponse } from "@bitwarden/admin-console/common"; import { OrganizationUserStatusType, ProviderUserStatusType, @@ -33,7 +33,7 @@ type BulkStatusDialogData = { users: Array; filteredUsers: Array; request: Promise>; - successfullMessage: string; + successfulMessage: string; }; @Component({ @@ -67,7 +67,7 @@ export class BulkStatusComponent implements OnInit { ); this.users = data.users.map((user) => { - let message = keyedErrors[user.id] ?? data.successfullMessage; + let message = keyedErrors[user.id] ?? data.successfulMessage; // eslint-disable-next-line if (!keyedFilteredUsers.hasOwnProperty(user.id)) { message = this.i18nService.t("bulkFilteredMessage"); diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html index 11c1ab2d2e3..ec1728c8836 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.html @@ -42,6 +42,7 @@ rel="noreferrer" appA11yTitle="{{ 'learnMore' | i18n }}" href="https://bitwarden.com/help/user-types-access-control/" + slot="end" > 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..fb11ad21c4c 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 @@ -13,8 +13,8 @@ import { takeUntil, } from "rxjs"; +import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { OrganizationUserStatusType, OrganizationUserType, @@ -23,12 +23,10 @@ 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"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { CollectionAdminService } from "../../../../../vault/core/collection-admin.service"; import { CollectionAdminView } from "../../../../../vault/core/views/collection-admin.view"; @@ -141,11 +139,11 @@ export class MemberDialogComponent implements OnDestroy { private collectionAdminService: CollectionAdminService, private groupService: GroupService, private userService: UserAdminService, - private organizationUserService: OrganizationUserService, + private organizationUserApiService: OrganizationUserApiService, private dialogService: DialogService, - private configService: ConfigService, private accountService: AccountService, organizationService: OrganizationService, + private toastService: ToastService, ) { this.organization$ = organizationService .get$(this.params.organizationId) @@ -174,15 +172,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 +199,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 +217,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 +273,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 +316,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), ), ); @@ -408,11 +377,11 @@ export class MemberDialogComponent implements OnDestroy { ) { this.permissionsGroup.value.manageUsers = true; (document.getElementById("manageUsers") as HTMLInputElement).checked = true; - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("accountRecoveryManageUsers"), - ); + this.toastService.showToast({ + variant: "info", + title: null, + message: this.i18nService.t("accountRecoveryManageUsers"), + }); } } @@ -421,11 +390,11 @@ export class MemberDialogComponent implements OnDestroy { if (this.formGroup.invalid) { if (this.tabIndex !== MemberDialogTab.Role) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("role")), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("fieldOnTabRequiresAttention", this.i18nService.t("role")), + }); } return; } @@ -433,11 +402,11 @@ export class MemberDialogComponent implements OnDestroy { const organization = await firstValueFrom(this.organization$); if (!organization.useCustomPermissions && this.customUserTypeSelected) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("customNonEnterpriseError"), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("customNonEnterpriseError"), + }); return; } @@ -484,11 +453,14 @@ export class MemberDialogComponent implements OnDestroy { await this.userService.invite(emails, userView); } - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.params.name), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t( + this.editMode ? "editedUserId" : "invitedUsers", + this.params.name, + ), + }); this.close(MemberDialogResult.Saved); }; @@ -519,16 +491,16 @@ export class MemberDialogComponent implements OnDestroy { } } - await this.organizationUserService.deleteOrganizationUser( + await this.organizationUserApiService.removeOrganizationUser( this.params.organizationId, this.params.organizationUserId, ); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("removedUserId", this.params.name), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("removedUserId", this.params.name), + }); this.close(MemberDialogResult.Deleted); }; @@ -556,16 +528,16 @@ export class MemberDialogComponent implements OnDestroy { } } - await this.organizationUserService.revokeOrganizationUser( + await this.organizationUserApiService.revokeOrganizationUser( this.params.organizationId, this.params.organizationUserId, ); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("revokedUserId", this.params.name), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("revokedUserId", this.params.name), + }); this.isRevoked = true; this.close(MemberDialogResult.Revoked); }; @@ -575,16 +547,16 @@ export class MemberDialogComponent implements OnDestroy { return; } - await this.organizationUserService.restoreOrganizationUser( + await this.organizationUserApiService.restoreOrganizationUser( this.params.organizationId, this.params.organizationUserId, ); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("restoredUserId", this.params.name), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("restoredUserId", this.params.name), + }); this.isRevoked = false; this.close(MemberDialogResult.Restored); }; @@ -621,7 +593,6 @@ export class MemberDialogComponent implements OnDestroy { function mapCollectionToAccessItemView( collection: CollectionAdminView, organization: Organization, - flexibleCollectionsV1Enabled: boolean, accessSelection?: CollectionAccessSelectionView, group?: GroupView, ): AccessItemView { @@ -630,9 +601,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/members/components/member-dialog/nested-checkbox.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/nested-checkbox.component.ts index d823240fe67..69819d69812 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/nested-checkbox.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/nested-checkbox.component.ts @@ -1,5 +1,5 @@ import { KeyValue } from "@angular/common"; -import { Component, EventEmitter, Input, Output, OnInit, OnDestroy } from "@angular/core"; +import { Component, Input, OnInit, OnDestroy } from "@angular/core"; import { FormControl, FormGroup } from "@angular/forms"; import { Subject, takeUntil } from "rxjs"; @@ -14,8 +14,6 @@ export class NestedCheckboxComponent implements OnInit, OnDestroy { @Input() parentId: string; @Input() checkboxes: FormGroup>>; - @Output() onSavedUser = new EventEmitter(); - @Output() onDeletedUser = new EventEmitter(); get parentIndeterminate() { return ( diff --git a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts index 291b0269427..ce605a6f5aa 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts @@ -17,7 +17,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { OrganizationUserResetPasswordService } from "../services/organization-user-reset-password/organization-user-reset-password.service"; @@ -31,7 +31,7 @@ export class ResetPasswordComponent implements OnInit, OnDestroy { @Input() email: string; @Input() id: string; @Input() organizationId: string; - @Output() onPasswordReset = new EventEmitter(); + @Output() passwordReset = new EventEmitter(); @ViewChild(PasswordStrengthComponent) passwordStrengthComponent: PasswordStrengthComponent; enforcedPolicyOptions: MasterPasswordPolicyOptions; @@ -50,6 +50,7 @@ export class ResetPasswordComponent implements OnInit, OnDestroy { private policyService: PolicyService, private logService: LogService, private dialogService: DialogService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -88,30 +89,30 @@ export class ResetPasswordComponent implements OnInit, OnDestroy { } this.platformUtilsService.copyToClipboard(value, { window: window }); - this.platformUtilsService.showToast( - "info", - null, - this.i18nService.t("valueCopied", this.i18nService.t("password")), - ); + this.toastService.showToast({ + variant: "info", + title: null, + message: this.i18nService.t("valueCopied", this.i18nService.t("password")), + }); } async submit() { // Validation if (this.newPassword == null || this.newPassword === "") { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPasswordRequired"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPasswordRequired"), + }); return false; } if (this.newPassword.length < Utils.minimumPasswordLength) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPasswordMinlength", Utils.minimumPasswordLength), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPasswordMinlength", Utils.minimumPasswordLength), + }); return false; } @@ -123,11 +124,11 @@ export class ResetPasswordComponent implements OnInit, OnDestroy { this.enforcedPolicyOptions, ) ) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPasswordPolicyRequirementsNotMet"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"), + }); return; } @@ -151,12 +152,12 @@ export class ResetPasswordComponent implements OnInit, OnDestroy { this.organizationId, ); await this.formPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("resetPasswordSuccess"), - ); - this.onPasswordReset.emit(); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("resetPasswordSuccess"), + }); + this.passwordReset.emit(); } catch (e) { this.logService.error(e); } diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.html b/apps/web/src/app/admin-console/organizations/members/members.component.html index 99afe8099a6..ae80130e03b 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.html +++ b/apps/web/src/app/admin-console/organizations/members/members.component.html @@ -5,7 +5,7 @@ [placeholder]="'searchMembers' | i18n" > - @@ -52,7 +52,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}

    {{ "noMembersInList" | i18n }}

    @@ -103,7 +103,12 @@
    - diff --git a/apps/web/src/app/admin-console/organizations/members/members.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts index 1376dd5ec0b..f4a5e738477 100644 --- a/apps/web/src/app/admin-console/organizations/members/members.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -13,15 +13,17 @@ import { switchMap, } from "rxjs"; +import { + OrganizationUserApiService, + OrganizationUserConfirmRequest, + OrganizationUserUserDetailsResponse, +} from "@bitwarden/admin-console/common"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; -import { OrganizationUserConfirmRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; -import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; import { PolicyApiServiceAbstraction as PolicyApiService } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { @@ -32,8 +34,10 @@ import { import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction"; -import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; +import { isNotSelfUpgradable, 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 { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -45,7 +49,11 @@ import { Collection } from "@bitwarden/common/vault/models/domain/collection"; import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response"; import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components"; -import { NewBasePeopleComponent } from "../../common/new-base.people.component"; +import { + ChangePlanDialogResultType, + openChangePlanDialog, +} from "../../../billing/organizations/change-plan-dialog.component"; +import { BaseMembersComponent } from "../../common/base-members.component"; import { PeopleTableDataSource } from "../../common/people-table-data-source"; import { GroupService } from "../core"; import { OrganizationUserView } from "../core/views/organization-user.view"; @@ -70,7 +78,7 @@ class MembersTableDataSource extends PeopleTableDataSource @Component({ templateUrl: "members.component.html", }) -export class MembersComponent extends NewBasePeopleComponent { +export class MembersComponent extends BaseMembersComponent { @ViewChild("resetPasswordTemplate", { read: ViewContainerRef, static: true }) resetPasswordModalRef: ViewContainerRef; @@ -86,6 +94,10 @@ export class MembersComponent extends NewBasePeopleComponent; + protected enableUpgradePasswordManagerSub$ = this.configService.getFeatureFlag$( + FeatureFlag.EnableUpgradePasswordManagerSub, + ); + // Fixed sizes used for cdkVirtualScroll protected rowHeight = 62; protected rowHeightClass = `tw-h-[62px]`; @@ -94,7 +106,6 @@ export class MembersComponent extends NewBasePeopleComponent>; // We don't need both groups and collections for the table, so only load one - const userPromise = this.organizationUserService.getAllUsers(this.organization.id, { + const userPromise = this.organizationUserApiService.getAllUsers(this.organization.id, { includeGroups: this.organization.useGroups, includeCollections: !this.organization.useGroups, }); @@ -259,20 +271,20 @@ export class MembersComponent extends NewBasePeopleComponent { - return this.organizationUserService.deleteOrganizationUser(this.organization.id, id); + removeUser(id: string): Promise { + return this.organizationUserApiService.removeOrganizationUser(this.organization.id, id); } revokeUser(id: string): Promise { - return this.organizationUserService.revokeOrganizationUser(this.organization.id, id); + return this.organizationUserApiService.revokeOrganizationUser(this.organization.id, id); } restoreUser(id: string): Promise { - return this.organizationUserService.restoreOrganizationUser(this.organization.id, id); + return this.organizationUserApiService.restoreOrganizationUser(this.organization.id, id); } reinviteUser(id: string): Promise { - return this.organizationUserService.postOrganizationUserReinvite(this.organization.id, id); + return this.organizationUserApiService.postOrganizationUserReinvite(this.organization.id, id); } async confirmUser(user: OrganizationUserView, publicKey: Uint8Array): Promise { @@ -280,7 +292,7 @@ export class MembersComponent extends NewBasePeopleComponent user.id), ); @@ -564,7 +600,7 @@ export class MembersComponent extends NewBasePeopleComponent { + comp.passwordReset.subscribe(() => { modal.close(); // 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 diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts index 637373b9367..b94cb4e926b 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.spec.ts @@ -1,8 +1,10 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { + OrganizationUserApiService, + OrganizationUserResetPasswordDetailsResponse, +} from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; -import { OrganizationUserResetPasswordDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response"; import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service"; @@ -24,7 +26,7 @@ describe("OrganizationUserResetPasswordService", () => { let cryptoService: MockProxy; let encryptService: MockProxy; let organizationService: MockProxy; - let organizationUserService: MockProxy; + let organizationUserApiService: MockProxy; let organizationApiService: MockProxy; let i18nService: MockProxy; @@ -32,7 +34,7 @@ describe("OrganizationUserResetPasswordService", () => { cryptoService = mock(); encryptService = mock(); organizationService = mock(); - organizationUserService = mock(); + organizationUserApiService = mock(); organizationApiService = mock(); i18nService = mock(); @@ -40,7 +42,7 @@ describe("OrganizationUserResetPasswordService", () => { cryptoService, encryptService, organizationService, - organizationUserService, + organizationUserApiService, organizationApiService, i18nService, ); @@ -112,7 +114,7 @@ describe("OrganizationUserResetPasswordService", () => { const mockOrgId = "test-org-id"; beforeEach(() => { - organizationUserService.getOrganizationUserResetPasswordDetails.mockResolvedValue( + organizationUserApiService.getOrganizationUserResetPasswordDetails.mockResolvedValue( new OrganizationUserResetPasswordDetailsResponse({ kdf: KdfType.PBKDF2_SHA256, kdfIterations: 5000, @@ -140,11 +142,11 @@ describe("OrganizationUserResetPasswordService", () => { it("should reset the user's master password", async () => { await sut.resetMasterPassword(mockNewMP, mockEmail, mockOrgUserId, mockOrgId); - expect(organizationUserService.putOrganizationUserResetPassword).toHaveBeenCalled(); + expect(organizationUserApiService.putOrganizationUserResetPassword).toHaveBeenCalled(); }); it("should throw an error if the user details are null", async () => { - organizationUserService.getOrganizationUserResetPasswordDetails.mockResolvedValue(null); + organizationUserApiService.getOrganizationUserResetPasswordDetails.mockResolvedValue(null); await expect( sut.resetMasterPassword(mockNewMP, mockEmail, mockOrgUserId, mockOrgId), ).rejects.toThrow(); diff --git a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts index 860fa6abc49..b3107f2b93a 100644 --- a/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts +++ b/apps/web/src/app/admin-console/organizations/members/services/organization-user-reset-password/organization-user-reset-password.service.ts @@ -1,13 +1,13 @@ import { Injectable } from "@angular/core"; +import { + OrganizationUserApiService, + OrganizationUserResetPasswordRequest, + OrganizationUserResetPasswordWithIdRequest, +} from "@bitwarden/admin-console/common"; import { UserKeyRotationDataProvider } from "@bitwarden/auth/common"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; -import { - OrganizationUserResetPasswordRequest, - OrganizationUserResetPasswordWithIdRequest, -} from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; import { Argon2KdfConfig, KdfConfig, @@ -33,7 +33,7 @@ export class OrganizationUserResetPasswordService private cryptoService: CryptoService, private encryptService: EncryptService, private organizationService: OrganizationService, - private organizationUserService: OrganizationUserService, + private organizationUserApiService: OrganizationUserApiService, private organizationApiService: OrganizationApiServiceAbstraction, private i18nService: I18nService, ) {} @@ -76,7 +76,7 @@ export class OrganizationUserResetPasswordService orgUserId: string, orgId: string, ): Promise { - const response = await this.organizationUserService.getOrganizationUserResetPasswordDetails( + const response = await this.organizationUserApiService.getOrganizationUserResetPasswordDetails( orgId, orgUserId, ); @@ -128,7 +128,11 @@ export class OrganizationUserResetPasswordService request.newMasterPasswordHash = newMasterKeyHash; // Change user's password - await this.organizationUserService.putOrganizationUserResetPassword(orgId, orgUserId, request); + await this.organizationUserApiService.putOrganizationUserResetPassword( + orgId, + orgUserId, + request, + ); } /** diff --git a/apps/web/src/app/admin-console/organizations/organization-routing.module.ts b/apps/web/src/app/admin-console/organizations/organization-routing.module.ts index 7427bbb481d..538cc45ac63 100644 --- a/apps/web/src/app/admin-console/organizations/organization-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/organization-routing.module.ts @@ -1,8 +1,7 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; -import { AuthGuard } from "@bitwarden/angular/auth/guards"; -import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; +import { authGuard } from "@bitwarden/angular/auth/guards"; import { canAccessOrgAdmin, canAccessGroupsTab, @@ -12,21 +11,20 @@ import { canAccessSettingsTab, } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { organizationPermissionsGuard } from "../../admin-console/organizations/guards/org-permissions.guard"; import { organizationRedirectGuard } from "../../admin-console/organizations/guards/org-redirect.guard"; import { OrganizationLayoutComponent } from "../../admin-console/organizations/layouts/organization-layout.component"; -import { GroupsComponent } from "../../admin-console/organizations/manage/groups.component"; -import { NewGroupsComponent } from "../../admin-console/organizations/manage/new-groups.component"; import { deepLinkGuard } from "../../auth/guards/deep-link.guard"; import { VaultModule } from "../../vault/org-vault/vault.module"; +import { GroupsComponent } from "./manage/groups.component"; + const routes: Routes = [ { path: ":organizationId", component: OrganizationLayoutComponent, - canActivate: [deepLinkGuard(), AuthGuard, organizationPermissionsGuard(canAccessOrgAdmin)], + canActivate: [deepLinkGuard(), authGuard, organizationPermissionsGuard(canAccessOrgAdmin)], children: [ { path: "", @@ -49,18 +47,14 @@ const routes: Routes = [ path: "members", loadChildren: () => import("./members").then((m) => m.MembersModule), }, - ...featureFlaggedRoute({ - defaultComponent: GroupsComponent, - flaggedComponent: NewGroupsComponent, - featureFlag: FeatureFlag.GroupsComponentRefactor, - routeOptions: { - path: "groups", - canActivate: [organizationPermissionsGuard(canAccessGroupsTab)], - data: { - titleId: "groups", - }, + { + component: GroupsComponent, + path: "groups", + canActivate: [organizationPermissionsGuard(canAccessGroupsTab)], + data: { + titleId: "groups", }, - }), + }, { path: "reporting", loadChildren: () => diff --git a/apps/web/src/app/admin-console/organizations/organization.module.ts b/apps/web/src/app/admin-console/organizations/organization.module.ts index 79f3a8e5f7d..459948d0f13 100644 --- a/apps/web/src/app/admin-console/organizations/organization.module.ts +++ b/apps/web/src/app/admin-console/organizations/organization.module.ts @@ -6,7 +6,6 @@ import { LooseComponentsModule } from "../../shared"; import { CoreOrganizationModule } from "./core"; import { GroupAddEditComponent } from "./manage/group-add-edit.component"; import { GroupsComponent } from "./manage/groups.component"; -import { NewGroupsComponent } from "./manage/new-groups.component"; import { OrganizationsRoutingModule } from "./organization-routing.module"; import { SharedOrganizationModule } from "./shared"; import { AccessSelectorModule } from "./shared/components/access-selector"; @@ -20,6 +19,6 @@ import { AccessSelectorModule } from "./shared/components/access-selector"; LooseComponentsModule, ScrollingModule, ], - declarations: [GroupsComponent, NewGroupsComponent, GroupAddEditComponent], + declarations: [GroupsComponent, GroupAddEditComponent], }) export class OrganizationModule {} diff --git a/apps/web/src/app/admin-console/organizations/policies/master-password.component.html b/apps/web/src/app/admin-console/organizations/policies/master-password.component.html index 85670f7d193..63a59208cc0 100644 --- a/apps/web/src/app/admin-console/organizations/policies/master-password.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/master-password.component.html @@ -50,6 +50,6 @@ - !@#$%^&* + !@#$%^&*
    diff --git a/apps/web/src/app/admin-console/organizations/policies/master-password.component.ts b/apps/web/src/app/admin-console/organizations/policies/master-password.component.ts index ff9caf557ae..14dd708389c 100644 --- a/apps/web/src/app/admin-console/organizations/policies/master-password.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/master-password.component.ts @@ -1,4 +1,4 @@ -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { FormBuilder, FormGroup, Validators } from "@angular/forms"; import { ControlsOf } from "@bitwarden/angular/types/controls-of"; @@ -21,7 +21,7 @@ export class MasterPasswordPolicy extends BasePolicy { selector: "policy-master-password", templateUrl: "master-password.component.html", }) -export class MasterPasswordPolicyComponent extends BasePolicyComponent { +export class MasterPasswordPolicyComponent extends BasePolicyComponent implements OnInit { MinPasswordLength = Utils.minimumPasswordLength; data: FormGroup> = this.formBuilder.group({ diff --git a/apps/web/src/app/admin-console/organizations/policies/password-generator.component.html b/apps/web/src/app/admin-console/organizations/policies/password-generator.component.html index 80df5fa2e6a..0e8b86de0fa 100644 --- a/apps/web/src/app/admin-console/organizations/policies/password-generator.component.html +++ b/apps/web/src/app/admin-console/organizations/policies/password-generator.component.html @@ -6,59 +6,70 @@
    - {{ "defaultType" | i18n }} - - + {{ "overridePasswordTypePolicy" | i18n }} + +
    -

    {{ "password" | i18n }}

    -
    - - {{ "minLength" | i18n }} - - + +
    +

    {{ "password" | i18n }}

    +
    + + {{ "minLength" | i18n }} + + +
    +
    + + {{ "minNumbers" | i18n }} + + + + {{ "minSpecial" | i18n }} + + +
    + + + A-Z + + + + a-z + + + + 0-9 + + + + !@#$%^&* +
    -
    - - {{ "minNumbers" | i18n }} - - - - {{ "minSpecial" | i18n }} - - + + +
    +

    {{ "passphrase" | i18n }}

    +
    + + {{ "minimumNumberOfWords" | i18n }} + + +
    + + + {{ "capitalize" | i18n }} + + + + {{ "includeNumber" | i18n }} +
    - - - A-Z - - - - a-z - - - - 0-9 - - - - !@#$%^&* - -

    {{ "passphrase" | i18n }}

    -
    - - {{ "minimumNumberOfWords" | i18n }} - - -
    - - - {{ "capitalize" | i18n }} - - - - {{ "includeNumber" | i18n }} -
    diff --git a/apps/web/src/app/admin-console/organizations/policies/password-generator.component.ts b/apps/web/src/app/admin-console/organizations/policies/password-generator.component.ts index 93124c42fa6..e1568da0487 100644 --- a/apps/web/src/app/admin-console/organizations/policies/password-generator.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/password-generator.component.ts @@ -1,8 +1,11 @@ import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { UntypedFormBuilder, Validators } from "@angular/forms"; +import { BehaviorSubject, map } from "rxjs"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { DefaultPassphraseBoundaries, DefaultPasswordBoundaries } from "@bitwarden/generator-core"; import { BasePolicy, BasePolicyComponent } from "./base-policy.component"; @@ -19,20 +22,59 @@ export class PasswordGeneratorPolicy extends BasePolicy { }) export class PasswordGeneratorPolicyComponent extends BasePolicyComponent { data = this.formBuilder.group({ - defaultType: [null], - minLength: [null, [Validators.min(5), Validators.max(128)]], + overridePasswordType: [null], + minLength: [ + null, + [ + Validators.min(DefaultPasswordBoundaries.length.min), + Validators.max(DefaultPasswordBoundaries.length.max), + ], + ], useUpper: [null], useLower: [null], useNumbers: [null], useSpecial: [null], - minNumbers: [null, [Validators.min(0), Validators.max(9)]], - minSpecial: [null, [Validators.min(0), Validators.max(9)]], - minNumberWords: [null, [Validators.min(3), Validators.max(20)]], + minNumbers: [ + null, + [ + Validators.min(DefaultPasswordBoundaries.minDigits.min), + Validators.max(DefaultPasswordBoundaries.minDigits.max), + ], + ], + minSpecial: [ + null, + [ + Validators.min(DefaultPasswordBoundaries.minSpecialCharacters.min), + Validators.max(DefaultPasswordBoundaries.minSpecialCharacters.max), + ], + ], + minNumberWords: [ + null, + [ + Validators.min(DefaultPassphraseBoundaries.numWords.min), + Validators.max(DefaultPassphraseBoundaries.numWords.max), + ], + ], capitalize: [null], includeNumber: [null], }); - defaultTypes: { name: string; value: string }[]; + overridePasswordTypeOptions: { name: string; value: string }[]; + + // These subjects cache visibility of the sub-options for passwords + // and passphrases; without them policy controls don't show up at all. + private showPasswordPolicies = new BehaviorSubject(true); + private showPassphrasePolicies = new BehaviorSubject(true); + + /** Emits `true` when the password policy options should be displayed */ + get showPasswordPolicies$() { + return this.showPasswordPolicies.asObservable(); + } + + /** Emits `true` when the passphrase policy options should be displayed */ + get showPassphrasePolicies$() { + return this.showPassphrasePolicies.asObservable(); + } constructor( private formBuilder: UntypedFormBuilder, @@ -40,10 +82,27 @@ export class PasswordGeneratorPolicyComponent extends BasePolicyComponent { ) { super(); - this.defaultTypes = [ + this.overridePasswordTypeOptions = [ { name: i18nService.t("userPreference"), value: null }, - { name: i18nService.t("password"), value: "password" }, + { name: i18nService.t("password"), value: PASSWORD_POLICY_VALUE }, { name: i18nService.t("passphrase"), value: "passphrase" }, ]; + + this.data.valueChanges + .pipe(isEnabled(PASSWORD_POLICY_VALUE), takeUntilDestroyed()) + .subscribe(this.showPasswordPolicies); + this.data.valueChanges + .pipe(isEnabled(PASSPHRASE_POLICY_VALUE), takeUntilDestroyed()) + .subscribe(this.showPassphrasePolicies); } } + +const PASSWORD_POLICY_VALUE = "password"; +const PASSPHRASE_POLICY_VALUE = "passphrase"; + +function isEnabled(enabledValue: string) { + return map((d: { overridePasswordType: string }) => { + const type = d?.overridePasswordType ?? enabledValue; + return type === enabledValue; + }); +} diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts index dc3b667511b..8f855e3c8c2 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts @@ -1,5 +1,12 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { ChangeDetectorRef, Component, Inject, ViewChild, ViewContainerRef } from "@angular/core"; +import { + AfterViewInit, + ChangeDetectorRef, + Component, + Inject, + ViewChild, + ViewContainerRef, +} from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; @@ -8,7 +15,7 @@ import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/po import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { BasePolicy, BasePolicyComponent } from "../policies"; @@ -28,7 +35,7 @@ export enum PolicyEditDialogResult { selector: "app-policy-edit", templateUrl: "policy-edit.component.html", }) -export class PolicyEditComponent { +export class PolicyEditComponent implements AfterViewInit { @ViewChild("policyForm", { read: ViewContainerRef, static: true }) policyFormRef: ViewContainerRef; @@ -51,6 +58,7 @@ export class PolicyEditComponent { private cdr: ChangeDetectorRef, private formBuilder: FormBuilder, private dialogRef: DialogRef, + private toastService: ToastService, ) {} get policy(): BasePolicy { return this.data.policy; @@ -88,7 +96,7 @@ export class PolicyEditComponent { try { request = await this.policyComponent.buildRequest(this.data.policiesEnabledMap); } catch (e) { - this.platformUtilsService.showToast("error", null, e.message); + this.toastService.showToast({ variant: "error", title: null, message: e.message }); return; } this.formPromise = this.policyApiService.putPolicy( @@ -97,11 +105,11 @@ export class PolicyEditComponent { request, ); await this.formPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("editedPolicyId", this.i18nService.t(this.data.policy.name)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("editedPolicyId", this.i18nService.t(this.data.policy.name)), + }); this.dialogRef.close(PolicyEditDialogResult.Saved); }; diff --git a/apps/web/src/app/admin-console/organizations/policies/reset-password.component.ts b/apps/web/src/app/admin-console/organizations/policies/reset-password.component.ts index fbf338ac1f2..6307ee13fa4 100644 --- a/apps/web/src/app/admin-console/organizations/policies/reset-password.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/reset-password.component.ts @@ -1,4 +1,4 @@ -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -22,7 +22,7 @@ export class ResetPasswordPolicy extends BasePolicy { selector: "policy-reset-password", templateUrl: "reset-password.component.html", }) -export class ResetPasswordPolicyComponent extends BasePolicyComponent { +export class ResetPasswordPolicyComponent extends BasePolicyComponent implements OnInit { data = this.formBuilder.group({ autoEnrollEnabled: false, }); 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 82f9a249939..8c97a176fb8 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 @@ -1,4 +1,4 @@ -import { Component, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { combineLatest, from, lastValueFrom, of, Subject, switchMap, takeUntil } from "rxjs"; @@ -10,13 +10,11 @@ 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"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { ApiKeyComponent } from "../../../auth/settings/security/api-key.component"; import { PurgeVaultComponent } from "../../../vault/settings/purge-vault.component"; @@ -27,7 +25,7 @@ import { DeleteOrganizationDialogResult, openDeleteOrganizationDialog } from "./ selector: "app-org-account", templateUrl: "account.component.html", }) -export class AccountComponent { +export class AccountComponent implements OnInit, OnDestroy { @ViewChild("apiKeyTemplate", { read: ViewContainerRef, static: true }) apiKeyModalRef: ViewContainerRef; @ViewChild("rotateApiKeyTemplate", { read: ViewContainerRef, static: true }) @@ -40,10 +38,6 @@ export class AccountComponent { 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,7 @@ export class AccountComponent { private organizationApiService: OrganizationApiServiceAbstraction, private dialogService: DialogService, private formBuilder: FormBuilder, - private configService: ConfigService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -174,7 +168,11 @@ export class AccountComponent { await this.organizationApiService.save(this.organizationId, request); - this.platformUtilsService.showToast("success", null, this.i18nService.t("organizationUpdated")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("organizationUpdated"), + }); }; submitCollectionManagement = async () => { @@ -191,11 +189,11 @@ export class AccountComponent { await this.organizationApiService.updateCollectionManagement(this.organizationId, request); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("updatedCollectionManagement"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("updatedCollectionManagement"), + }); }; async deleteOrganization() { diff --git a/apps/web/src/app/admin-console/organizations/settings/components/delete-organization-dialog.component.ts b/apps/web/src/app/admin-console/organizations/settings/components/delete-organization-dialog.component.ts index 7a88905449d..b5e1baf70f1 100644 --- a/apps/web/src/app/admin-console/organizations/settings/components/delete-organization-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/components/delete-organization-dialog.component.ts @@ -14,7 +14,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { UserVerificationModule } from "../../../../auth/shared/components/user-verification"; import { SharedModule } from "../../../../shared/shared.module"; @@ -94,6 +94,7 @@ export class DeleteOrganizationDialogComponent implements OnInit, OnDestroy { private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, private formBuilder: FormBuilder, + private toastService: ToastService, ) {} ngOnDestroy(): void { @@ -121,11 +122,11 @@ export class DeleteOrganizationDialogComponent implements OnInit, OnDestroy { .buildRequest(this.formGroup.value.secret) .then((request) => this.organizationApiService.delete(this.organization.id, request)); - this.platformUtilsService.showToast( - "success", - this.i18nService.t("organizationDeleted"), - this.i18nService.t("organizationDeletedDesc"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("organizationDeleted"), + message: this.i18nService.t("organizationDeletedDesc"), + }); this.dialogRef.close(DeleteOrganizationDialogResult.Deleted); }; diff --git a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts index 0e00c31a69e..39d29741e76 100644 --- a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts @@ -1,5 +1,5 @@ import { DialogRef } from "@angular/cdk/dialog"; -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { concatMap, takeUntil, map, lastValueFrom } from "rxjs"; import { first, tap } from "rxjs/operators"; @@ -24,7 +24,7 @@ import { TwoFactorVerifyComponent } from "../../../auth/settings/two-factor-veri templateUrl: "../../../auth/settings/two-factor-setup.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent { +export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent implements OnInit { tabbedHeader = false; constructor( dialogService: DialogService, diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-dialog.stories.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-dialog.stories.ts new file mode 100644 index 00000000000..efe666dae2e --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-dialog.stories.ts @@ -0,0 +1,74 @@ +import { Meta, StoryObj } from "@storybook/angular"; + +import { AccessSelectorComponent, PermissionMode } from "./access-selector.component"; +import { AccessItemType, AccessItemValue } from "./access-selector.models"; +import { default as baseComponentDefinition } from "./access-selector.stories"; +import { actionsData, itemsFactory } from "./storybook-utils"; + +/** + * Displays the Access Selector in a dialog. + */ +export default { + title: "Web/Organizations/Access Selector/Dialog", + decorators: baseComponentDefinition.decorators, +} as Meta; + +type Story = StoryObj; + +const render: Story["render"] = (args) => ({ + props: { + items: [], + valueChanged: actionsData.onValueChanged, + initialValue: [], + ...args, + }, + template: ` + + Access selector + + + + + + + + + + `, +}); + +const dialogAccessItems = itemsFactory(10, AccessItemType.Collection); + +export const Dialog: Story = { + args: { + permissionMode: PermissionMode.Edit, + showMemberRoles: false, + showGroupColumn: true, + columnHeader: "Collection", + selectorLabelText: "Select Collections", + selectorHelpText: "Some helper text describing what this does", + emptySelectionText: "No collections added", + disabled: false, + initialValue: [] as any[], + items: dialogAccessItems, + }, + render, +}; diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-reactive.stories.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-reactive.stories.ts new file mode 100644 index 00000000000..ec7c378f19c --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector-reactive.stories.ts @@ -0,0 +1,64 @@ +import { FormBuilder, FormControl, FormGroup } from "@angular/forms"; +import { Meta, StoryObj } from "@storybook/angular"; + +import { AccessSelectorComponent, PermissionMode } from "./access-selector.component"; +import { AccessItemType, AccessItemValue } from "./access-selector.models"; +import { default as baseComponentDefinition } from "./access-selector.stories"; +import { actionsData, itemsFactory } from "./storybook-utils"; + +/** + * Displays the Access Selector embedded in a reactive form. + */ +export default { + title: "Web/Organizations/Access Selector/Reactive form", + decorators: baseComponentDefinition.decorators, + argTypes: { + formObj: { table: { disable: true } }, + }, +} as Meta; + +type FormObj = { formObj: FormGroup<{ formItems: FormControl }> }; +type Story = StoryObj; + +const fb = new FormBuilder(); + +const render: Story["render"] = (args) => ({ + props: { + items: [], + onSubmit: actionsData.onSubmit, + ...args, + }, + template: ` + + + + +`, +}); + +const sampleMembers = itemsFactory(10, AccessItemType.Member); +const sampleGroups = itemsFactory(6, AccessItemType.Group); + +export const ReactiveForm: Story = { + args: { + formObj: fb.group({ formItems: [[{ id: "1g", type: AccessItemType.Group }]] }), + permissionMode: PermissionMode.Edit, + showMemberRoles: false, + columnHeader: "Groups/Members", + selectorLabelText: "Select groups and members", + selectorHelpText: + "Permissions set for a member will replace permissions set by that member's group", + emptySelectionText: "No members or groups added", + items: sampleGroups.concat(sampleMembers), + }, + render, +}; diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html index 9077bd747fd..aff5d25ee08 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.html @@ -85,7 +85,7 @@ -
    diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.spec.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.spec.ts index 90e652675c4..592995f88fc 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.spec.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.spec.ts @@ -205,7 +205,7 @@ describe("AccessSelectorComponent", () => { labelName: "Member 1", listName: "Member 1 (member1@email.com)", email: "member1@email.com", - role: OrganizationUserType.Manager, + role: OrganizationUserType.User, status: OrganizationUserStatusType.Confirmed, }, ]; diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts index 3d70ee18df3..08381b7368b 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.component.ts @@ -59,7 +59,7 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On /** * Updates the enabled/disabled state of provided row form group based on the item's readonly state. * If a row is enabled, it also updates the enabled/disabled state of the permission control - * based on the item's accessAllItems state and the current value of `permissionMode`. + * based on the current value of `permissionMode`. * @param controlRow - The form group for the row to update * @param item - The access item that is represented by the row */ @@ -74,7 +74,7 @@ export class AccessSelectorComponent implements ControlValueAccessor, OnInit, On controlRow.enable(); // The enable() above also enables the permission control, so we need to disable it again - // Disable permission control if accessAllItems is enabled or not in Edit mode + // Disable permission control if not in Edit mode if (this.permissionMode != PermissionMode.Edit) { controlRow.controls.permission.disable(); } diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts index d0d05004c4c..429b62ed0cc 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts @@ -1,4 +1,4 @@ -import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; +import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common"; import { OrganizationUserStatusType, OrganizationUserType, diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.stories.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.stories.ts index f44b5d251ec..095be1df966 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.stories.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.stories.ts @@ -1,13 +1,8 @@ import { importProvidersFrom } from "@angular/core"; -import { FormBuilder, FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { action } from "@storybook/addon-actions"; -import { applicationConfig, Meta, moduleMetadata, Story } from "@storybook/angular"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { - OrganizationUserStatusType, - OrganizationUserType, -} from "@bitwarden/common/admin-console/enums"; import { AvatarModule, BadgeModule, @@ -21,10 +16,20 @@ import { import { PreloadedEnglishI18nModule } from "../../../../../core/tests"; -import { AccessSelectorComponent } from "./access-selector.component"; -import { AccessItemType, AccessItemView, CollectionPermission } from "./access-selector.models"; +import { AccessSelectorComponent, PermissionMode } from "./access-selector.component"; +import { AccessItemType, AccessItemValue, CollectionPermission } from "./access-selector.models"; +import { actionsData, itemsFactory } from "./storybook-utils"; import { UserTypePipe } from "./user-type.pipe"; +/** + * The Access Selector is used to view and edit: + * - member and group access to collections + * - members assigned to groups + * + * It is highly configurable in order to display these relationships from each perspective. For example, you can + * manage member-group relationships from the perspective of a particular member (showing all their groups) or a + * particular group (showing all its members). + */ export default { title: "Web/Organizations/Access Selector", decorators: [ @@ -49,127 +54,37 @@ export default { providers: [importProvidersFrom(PreloadedEnglishI18nModule)], }), ], - parameters: {}, - argTypes: { - formObj: { table: { disable: true } }, - }, } as Meta; -const actionsData = { - onValueChanged: action("onValueChanged"), - onSubmit: action("onSubmit"), -}; - -/** - * Factory to help build semi-realistic looking items - * @param n - The number of items to build - * @param type - Which type to build - */ -const itemsFactory = (n: number, type: AccessItemType) => { - return [...Array(n)].map((_: unknown, id: number) => { - const item: AccessItemView = { - id: id.toString(), - type: type, - } as AccessItemView; - - switch (item.type) { - case AccessItemType.Collection: - item.labelName = item.listName = `Collection ${id}`; - item.id = item.id + "c"; - item.parentGrouping = "Collection Parent Group " + ((id % 2) + 1); - break; - case AccessItemType.Group: - item.labelName = item.listName = `Group ${id}`; - item.id = item.id + "g"; - break; - case AccessItemType.Member: - item.id = item.id + "m"; - item.email = `member${id}@email.com`; - item.status = id % 3 == 0 ? 0 : 2; - item.labelName = item.status == 2 ? `Member ${id}` : item.email; - item.listName = item.status == 2 ? `${item.labelName} (${item.email})` : item.email; - item.role = id % 5; - break; - } - - return item; - }); -}; +type Story = StoryObj; const sampleMembers = itemsFactory(10, AccessItemType.Member); const sampleGroups = itemsFactory(6, AccessItemType.Group); -const StandaloneAccessSelectorTemplate: Story = ( - args: AccessSelectorComponent, -) => ({ +const render: Story["render"] = (args) => ({ props: { - items: [], valueChanged: actionsData.onValueChanged, - initialValue: [], ...args, }, template: ` - -`, + + `, }); -const DialogAccessSelectorTemplate: Story = ( - args: AccessSelectorComponent, -) => ({ - props: { - items: [], - valueChanged: actionsData.onValueChanged, - initialValue: [], - ...args, - }, - template: ` - - Access selector - - - - - - - - - -`, -}); - -const dialogAccessItems = itemsFactory(10, AccessItemType.Collection); - -const memberCollectionAccessItems = itemsFactory(3, AccessItemType.Collection).concat([ +const memberCollectionAccessItems = itemsFactory(5, AccessItemType.Collection).concat([ + // These represent collection access via a group { id: "c1-group1", type: AccessItemType.Collection, @@ -190,184 +105,150 @@ const memberCollectionAccessItems = itemsFactory(3, AccessItemType.Collection).c }, ]); -export const Dialog = DialogAccessSelectorTemplate.bind({}); -Dialog.args = { - permissionMode: "edit", - showMemberRoles: false, - showGroupColumn: true, - columnHeader: "Collection", - selectorLabelText: "Select Collections", - selectorHelpText: "Some helper text describing what this does", - emptySelectionText: "No collections added", - disabled: false, - initialValue: [], - items: dialogAccessItems, -}; -Dialog.story = { - parameters: { - docs: { - storyDescription: ` - Example of an access selector for modifying the collections a member has access to inside of a dialog. - `, - }, +// Simulate the current user not having permission to change access to this collection +// TODO: currently the member dialog duplicates the AccessItemValue.permission on the +// AccessItemView.readonlyPermission, this will be refactored to reduce this duplication: +// https://bitwarden.atlassian.net/browse/PM-11590 +memberCollectionAccessItems[4].readonly = true; +memberCollectionAccessItems[4].readonlyPermission = CollectionPermission.Manage; + +/** + * Displays a member's collection access. + * + * This is currently used in the **Member dialog -> Collections tab**. Note that it includes collection access that the + * member has via a group. + * + * This is also used in the **Groups dialog -> Collections tab** to show a group's collection access and in this + * case the Group column is hidden. + */ +export const MemberCollectionAccess: Story = { + args: { + permissionMode: PermissionMode.Edit, + showMemberRoles: false, + showGroupColumn: true, + columnHeader: "Collection", + selectorLabelText: "Select Collections", + selectorHelpText: "Some helper text describing what this does", + emptySelectionText: "No collections added", + disabled: false, + initialValue: [ + { + id: "4c", + type: AccessItemType.Collection, + permission: CollectionPermission.Manage, + }, + { + id: "2c", + type: AccessItemType.Collection, + permission: CollectionPermission.Edit, + }, + ], + items: memberCollectionAccessItems, }, + render, }; -export const MemberCollectionAccess = StandaloneAccessSelectorTemplate.bind({}); -MemberCollectionAccess.args = { - permissionMode: "edit", - showMemberRoles: false, - showGroupColumn: true, - columnHeader: "Collection", - selectorLabelText: "Select Collections", - selectorHelpText: "Some helper text describing what this does", - emptySelectionText: "No collections added", - disabled: false, - initialValue: [], - items: memberCollectionAccessItems, -}; -MemberCollectionAccess.story = { - parameters: { - docs: { - storyDescription: ` - Example of an access selector for modifying the collections a member has access to. - Includes examples of a readonly group and member that cannot be edited. - `, - }, +/** + * Displays the groups a member is assigned to. + * + * This is currently used in the **Member dialog -> Groups tab**. + */ +export const MemberGroupAccess: Story = { + args: { + permissionMode: PermissionMode.Hidden, + showMemberRoles: false, + columnHeader: "Groups", + selectorLabelText: "Select Groups", + selectorHelpText: "Some helper text describing what this does", + emptySelectionText: "No groups added", + disabled: false, + initialValue: [ + { id: "3g", type: AccessItemType.Group }, + { id: "0g", type: AccessItemType.Group }, + ], + items: itemsFactory(4, AccessItemType.Group).concat([ + { + id: "admin", + type: AccessItemType.Group, + listName: "Admin Group", + labelName: "Admin Group", + }, + ]), }, + render, }; -export const MemberGroupAccess = StandaloneAccessSelectorTemplate.bind({}); -MemberGroupAccess.args = { - permissionMode: "readonly", - showMemberRoles: false, - columnHeader: "Groups", - selectorLabelText: "Select Groups", - selectorHelpText: "Some helper text describing what this does", - emptySelectionText: "No groups added", - disabled: false, - initialValue: [{ id: "3g" }, { id: "0g" }], - items: itemsFactory(4, AccessItemType.Group).concat([ - { - id: "admin", - type: AccessItemType.Group, - listName: "Admin Group", - labelName: "Admin Group", - }, - ]), -}; -MemberGroupAccess.story = { - parameters: { - docs: { - storyDescription: ` - Example of an access selector for selecting which groups an individual member belongs too. - `, - }, +/** + * Displays the members assigned to a group. + * + * This is currently used in the **Group dialog -> Members tab**. + */ +export const GroupMembersAccess: Story = { + args: { + permissionMode: PermissionMode.Hidden, + showMemberRoles: true, + columnHeader: "Members", + selectorLabelText: "Select Members", + selectorHelpText: "Some helper text describing what this does", + emptySelectionText: "No members added", + disabled: false, + initialValue: [ + { id: "2m", type: AccessItemType.Member }, + { id: "0m", type: AccessItemType.Member }, + ], + items: sampleMembers, }, + render, }; -export const GroupMembersAccess = StandaloneAccessSelectorTemplate.bind({}); -GroupMembersAccess.args = { - permissionMode: "hidden", - showMemberRoles: true, - columnHeader: "Members", - selectorLabelText: "Select Members", - selectorHelpText: "Some helper text describing what this does", - emptySelectionText: "No members added", - disabled: false, - initialValue: [{ id: "2m" }, { id: "0m" }], - items: sampleMembers, -}; -GroupMembersAccess.story = { - parameters: { - docs: { - storyDescription: ` - Example of an access selector for selecting which members belong to an specific group. - `, - }, +/** + * Displays the members and groups assigned to a collection. + * + * This is currently used in the **Collection dialog -> Access tab**. + */ +export const CollectionAccess: Story = { + args: { + permissionMode: PermissionMode.Edit, + showMemberRoles: false, + columnHeader: "Groups/Members", + selectorLabelText: "Select groups and members", + selectorHelpText: + "Permissions set for a member will replace permissions set by that member's group", + emptySelectionText: "No members or groups added", + disabled: false, + initialValue: [ + { id: "3g", type: AccessItemType.Group, permission: CollectionPermission.EditExceptPass }, + { id: "0m", type: AccessItemType.Member, permission: CollectionPermission.View }, + { id: "7m", type: AccessItemType.Member, permission: CollectionPermission.Manage }, + ], + items: sampleGroups.concat(sampleMembers), }, + render, }; -export const CollectionAccess = StandaloneAccessSelectorTemplate.bind({}); -CollectionAccess.args = { - permissionMode: "edit", - showMemberRoles: false, - columnHeader: "Groups/Members", - selectorLabelText: "Select groups and members", - selectorHelpText: - "Permissions set for a member will replace permissions set by that member's group", - emptySelectionText: "No members or groups added", - disabled: false, - initialValue: [ - { id: "3g", permission: CollectionPermission.EditExceptPass }, - { id: "0m", permission: CollectionPermission.View }, - ], - items: sampleGroups.concat(sampleMembers).concat([ - { - id: "admin-group", - type: AccessItemType.Group, - listName: "Admin Group", - labelName: "Admin Group", - readonly: true, - }, - { - id: "admin-member", - type: AccessItemType.Member, - listName: "Admin Member (admin@email.com)", - labelName: "Admin Member", - status: OrganizationUserStatusType.Confirmed, - role: OrganizationUserType.Admin, - email: "admin@email.com", - readonly: true, - }, - ]), -}; -GroupMembersAccess.story = { - parameters: { - docs: { - storyDescription: ` - Example of an access selector for selecting which members/groups have access to a specific collection. - `, - }, +// TODO: currently the collection dialog duplicates the AccessItemValue.permission on the +// AccessItemView.readonlyPermission, this will be refactored to reduce this duplication: +// https://bitwarden.atlassian.net/browse/PM-11590 +const disabledMembers = itemsFactory(3, AccessItemType.Member); +disabledMembers[1].readonlyPermission = CollectionPermission.Manage; +disabledMembers[2].readonlyPermission = CollectionPermission.View; + +const disabledGroups = itemsFactory(2, AccessItemType.Group); +disabledGroups[0].readonlyPermission = CollectionPermission.ViewExceptPass; + +/** + * Displays the members and groups assigned to a collection when the control is in a disabled state. + */ +export const DisabledCollectionAccess: Story = { + args: { + ...CollectionAccess.args, + disabled: true, + items: disabledGroups.concat(disabledMembers), + initialValue: [ + { id: "1m", type: AccessItemType.Member, permission: CollectionPermission.Manage }, + { id: "2m", type: AccessItemType.Member, permission: CollectionPermission.View }, + { id: "0g", type: AccessItemType.Group, permission: CollectionPermission.ViewExceptPass }, + ], }, -}; - -const fb = new FormBuilder(); - -const ReactiveFormAccessSelectorTemplate: Story = ( - args: AccessSelectorComponent, -) => ({ - props: { - items: [], - onSubmit: actionsData.onSubmit, - ...args, - }, - template: ` -
    - - -
    -`, -}); - -export const ReactiveForm = ReactiveFormAccessSelectorTemplate.bind({}); -ReactiveForm.args = { - formObj: fb.group({ formItems: [[{ id: "1g" }]] }), - permissionMode: "edit", - showMemberRoles: false, - columnHeader: "Groups/Members", - selectorLabelText: "Select groups and members", - selectorHelpText: - "Permissions set for a member will replace permissions set by that member's group", - emptySelectionText: "No members or groups added", - items: sampleGroups.concat(sampleMembers), + render, }; diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/storybook-utils.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/storybook-utils.ts new file mode 100644 index 00000000000..fb8bdef1d8c --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/storybook-utils.ts @@ -0,0 +1,44 @@ +import { action } from "@storybook/addon-actions"; + +import { AccessItemType, AccessItemView } from "./access-selector.models"; + +export const actionsData = { + onValueChanged: action("onValueChanged"), + onSubmit: action("onSubmit"), +}; + +/** + * Factory to help build semi-realistic looking items + * @param n - The number of items to build + * @param type - Which type to build + */ +export const itemsFactory = (n: number, type: AccessItemType) => { + return [...Array(n)].map((_: unknown, id: number) => { + const item: AccessItemView = { + id: id.toString(), + type: type, + } as AccessItemView; + + switch (item.type) { + case AccessItemType.Collection: + item.labelName = item.listName = `Collection ${id}`; + item.id = item.id + "c"; + item.parentGrouping = "Collection Parent Group " + ((id % 2) + 1); + break; + case AccessItemType.Group: + item.labelName = item.listName = `Group ${id}`; + item.id = item.id + "g"; + break; + case AccessItemType.Member: + item.id = item.id + "m"; + item.email = `member${id}@email.com`; + item.status = id % 3 == 0 ? 0 : 2; + item.labelName = item.status == 2 ? `Member ${id}` : item.email; + item.listName = item.status == 2 ? `${item.labelName} (${item.email})` : item.email; + item.role = id % 5; + break; + } + + return item; + }); +}; diff --git a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html index e7eb29a3ac7..7640e1c7366 100644 --- a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html +++ b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.html @@ -7,7 +7,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}

    diff --git a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts index e5733e262f6..239ac835f51 100644 --- a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts +++ b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts @@ -1,8 +1,14 @@ import { Component } from "@angular/core"; import { Params } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { BaseAcceptComponent } from "../../../common/base.accept.component"; +/* + * This component is responsible for handling the acceptance of a families plan sponsorship invite. + * "Bitwarden allows all members of Enterprise Organizations to redeem a complimentary Families Plan with their + * personal email address." - https://bitwarden.com/learning/free-families-plan-for-enterprise/ + */ @Component({ selector: "app-accept-family-sponsorship", templateUrl: "accept-family-sponsorship.component.html", @@ -25,9 +31,32 @@ export class AcceptFamilySponsorshipComponent extends BaseAcceptComponent { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["/login"], { queryParams: { email: qParams.email } }); } else { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate([this.registerRoute], { queryParams: { email: qParams.email } }); + // TODO: update logic when email verification flag is removed + let queryParams: Params; + let registerRoute = await firstValueFrom(this.registerRoute$); + if (registerRoute === "/register") { + queryParams = { + email: qParams.email, + }; + } else if (registerRoute === "/signup") { + // We have to override the base component route as we don't need users to + // complete email verification if they are coming directly an emailed invite. + + // TODO: in the future, to allow users to enter a name, consider sending all invite users to + // start registration page with prefilled email and a named token to be passed directly + // along to the finish-signup page without requiring email verification as + // we can treat the existence of the token as a form of email verification. + + registerRoute = "/finish-signup"; + queryParams = { + email: qParams.email, + orgSponsoredFreeFamilyPlanToken: qParams.token, + }; + } + + await this.router.navigate([registerRoute], { + queryParams: queryParams, + }); } } } diff --git a/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts b/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts index 2c15ab5ac80..f857eb63764 100644 --- a/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts +++ b/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts @@ -6,6 +6,7 @@ import { first, map, takeUntil } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { OrganizationSponsorshipRedeemRequest } from "@bitwarden/common/admin-console/models/request/organization/organization-sponsorship-redeem.request"; import { PlanSponsorshipType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; @@ -13,7 +14,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { OrganizationPlansComponent } from "../../../billing"; import { SharedModule } from "../../../shared"; @@ -67,6 +68,7 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy { private organizationService: OrganizationService, private dialogService: DialogService, private formBuilder: FormBuilder, + private toastService: ToastService, ) {} async ngOnInit() { @@ -75,12 +77,12 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy { this.route.queryParams.pipe(first()).subscribe(async (qParams) => { const error = qParams.token == null; if (error) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("sponsoredFamiliesAcceptFailed"), - { timeout: 10000 }, - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("sponsoredFamiliesAcceptFailed"), + timeout: 10000, + }); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["/"]); @@ -95,7 +97,12 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy { }); this.existingFamilyOrganizations$ = this.organizationService.organizations$.pipe( - map((orgs) => orgs.filter((o) => o.productTierType === ProductTierType.Families)), + map((orgs) => + orgs.filter( + (o) => + o.productTierType === ProductTierType.Families && o.type === OrganizationUserType.Owner, + ), + ), ); this.existingFamilyOrganizations$.pipe(takeUntil(this._destroy)).subscribe((orgs) => { @@ -133,11 +140,11 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy { request.sponsoredOrganizationId = organizationId; await this.apiService.postRedeemSponsorship(this.token, request); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("sponsoredFamiliesOfferRedeemed"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("sponsoredFamiliesOfferRedeemed"), + }); await this.syncService.fullSync(true); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. diff --git a/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts index cab6189c456..3ea2a8f43e2 100644 --- a/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts @@ -1,4 +1,4 @@ -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -19,7 +19,10 @@ import { ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent templateUrl: "../../../tools/reports/pages/exposed-passwords-report.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportComponent { +export class ExposedPasswordsReportComponent + extends BaseExposedPasswordsReportComponent + implements OnInit +{ manageableCiphers: Cipher[]; constructor( diff --git a/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts index abfbd45f38a..6eb8889dc2f 100644 --- a/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts @@ -1,4 +1,4 @@ -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -18,7 +18,10 @@ import { InactiveTwoFactorReportComponent as BaseInactiveTwoFactorReportComponen templateUrl: "../../../tools/reports/pages/inactive-two-factor-report.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class InactiveTwoFactorReportComponent extends BaseInactiveTwoFactorReportComponent { +export class InactiveTwoFactorReportComponent + extends BaseInactiveTwoFactorReportComponent + implements OnInit +{ constructor( cipherService: CipherService, modalService: ModalService, diff --git a/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts index 76d783b6664..0fea1f0a584 100644 --- a/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts @@ -1,4 +1,4 @@ -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -18,7 +18,10 @@ import { ReusedPasswordsReportComponent as BaseReusedPasswordsReportComponent } templateUrl: "../../../tools/reports/pages/reused-passwords-report.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportComponent { +export class ReusedPasswordsReportComponent + extends BaseReusedPasswordsReportComponent + implements OnInit +{ manageableCiphers: Cipher[]; constructor( diff --git a/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts index 7f6f08fb96f..c520d3dad68 100644 --- a/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts @@ -1,10 +1,11 @@ -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -17,7 +18,10 @@ import { UnsecuredWebsitesReportComponent as BaseUnsecuredWebsitesReportComponen templateUrl: "../../../tools/reports/pages/unsecured-websites-report.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class UnsecuredWebsitesReportComponent extends BaseUnsecuredWebsitesReportComponent { +export class UnsecuredWebsitesReportComponent + extends BaseUnsecuredWebsitesReportComponent + implements OnInit +{ constructor( cipherService: CipherService, modalService: ModalService, @@ -26,6 +30,7 @@ export class UnsecuredWebsitesReportComponent extends BaseUnsecuredWebsitesRepor passwordRepromptService: PasswordRepromptService, i18nService: I18nService, syncService: SyncService, + collectionService: CollectionService, ) { super( cipherService, @@ -34,6 +39,7 @@ export class UnsecuredWebsitesReportComponent extends BaseUnsecuredWebsitesRepor passwordRepromptService, i18nService, syncService, + collectionService, ); } diff --git a/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts index 0ac21294782..06e6fdf0a75 100644 --- a/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts @@ -1,4 +1,4 @@ -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -19,7 +19,10 @@ import { WeakPasswordsReportComponent as BaseWeakPasswordsReportComponent } from templateUrl: "../../../tools/reports/pages/weak-passwords-report.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportComponent { +export class WeakPasswordsReportComponent + extends BaseWeakPasswordsReportComponent + implements OnInit +{ manageableCiphers: Cipher[]; constructor( diff --git a/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts b/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts index e262fa51ffe..17e608df3ee 100644 --- a/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts +++ b/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts @@ -1,6 +1,8 @@ +import { + OrganizationUserApiService, + OrganizationUserResetPasswordEnrollmentRequest, +} from "@bitwarden/admin-console/common"; import { UserVerificationDialogComponent } from "@bitwarden/auth/angular"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; -import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VerificationWithSecret } from "@bitwarden/common/auth/types/verification"; @@ -8,7 +10,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { OrganizationUserResetPasswordService } from "../members/services/organization-user-reset-password/organization-user-reset-password.service"; @@ -23,12 +25,13 @@ export class EnrollMasterPasswordReset { dialogService: DialogService, data: EnrollMasterPasswordResetData, resetPasswordService: OrganizationUserResetPasswordService, - organizationUserService: OrganizationUserService, + organizationUserApiService: OrganizationUserApiService, platformUtilsService: PlatformUtilsService, i18nService: I18nService, syncService: SyncService, logService: LogService, userVerificationService: UserVerificationService, + toastService: ToastService, ) { const result = await UserVerificationDialogComponent.open(dialogService, { title: "enrollAccountRecovery", @@ -49,7 +52,7 @@ export class EnrollMasterPasswordReset { // Process the enrollment request, which is an endpoint that is // gated by a server-side check of the master password hash - await organizationUserService.putOrganizationUserResetPasswordEnrollment( + await organizationUserApiService.putOrganizationUserResetPasswordEnrollment( data.organization.id, data.organization.userId, request, @@ -71,7 +74,11 @@ export class EnrollMasterPasswordReset { // Enrollment succeeded try { - platformUtilsService.showToast("success", null, i18nService.t("enrollPasswordResetSuccess")); + toastService.showToast({ + variant: "success", + title: null, + message: i18nService.t("enrollPasswordResetSuccess"), + }); await syncService.fullSync(true); } catch (e) { logService.error(e); diff --git a/apps/web/src/app/admin-console/providers/providers.component.html b/apps/web/src/app/admin-console/providers/providers.component.html index d07342c85c2..560c164415c 100644 --- a/apps/web/src/app/admin-console/providers/providers.component.html +++ b/apps/web/src/app/admin-console/providers/providers.component.html @@ -3,7 +3,7 @@

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

    @@ -20,7 +20,7 @@ title="{{ 'providerIsDisabled' | i18n }}" aria-hidden="true" > - {{ "providerIsDisabled" | i18n }} + {{ "providerIsDisabled" | i18n }} diff --git a/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts b/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts index 0550820cda4..dc6fa099610 100644 --- a/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts +++ b/apps/web/src/app/admin-console/providers/verify-recover-delete-provider.component.ts @@ -7,6 +7,7 @@ import { ProviderVerifyRecoverDeleteRequest } from "@bitwarden/common/admin-cons 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 { ToastService } from "@bitwarden/components"; @Component({ selector: "app-verify-recover-delete-provider", @@ -27,6 +28,7 @@ export class VerifyRecoverDeleteProviderComponent implements OnInit { private i18nService: I18nService, private route: ActivatedRoute, private logService: LogService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -48,11 +50,11 @@ export class VerifyRecoverDeleteProviderComponent implements OnInit { request, ); await this.formPromise; - this.platformUtilsService.showToast( - "success", - this.i18nService.t("providerDeleted"), - this.i18nService.t("providerDeletedDesc"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("providerDeleted"), + message: this.i18nService.t("providerDeletedDesc"), + }); await this.router.navigate(["/"]); } catch (e) { this.logService.error(e); diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 1c5527504d9..828fe8ea3fe 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -2,17 +2,7 @@ import { DOCUMENT } from "@angular/common"; import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core"; import { NavigationEnd, Router } from "@angular/router"; import * as jq from "jquery"; -import { - Subject, - combineLatest, - filter, - firstValueFrom, - map, - switchMap, - takeUntil, - timeout, - timer, -} from "rxjs"; +import { Subject, filter, firstValueFrom, map, takeUntil, timeout } from "rxjs"; import { LogoutReason } from "@bitwarden/auth/common"; import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; @@ -25,8 +15,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -58,7 +46,6 @@ import { const BroadcasterSubscriptionId = "AppComponent"; const IdleTimeout = 60000 * 10; // 10 minutes -const PaymentMethodWarningsRefresh = 60000; // 1 Minute @Component({ selector: "app-root", @@ -69,7 +56,6 @@ export class AppComponent implements OnDestroy, OnInit { private idleTimer: number = null; private isIdle = false; private destroy$ = new Subject(); - private paymentMethodWarningsRefresh$ = timer(0, PaymentMethodWarningsRefresh); constructor( @Inject(DOCUMENT) private document: Document, @@ -94,11 +80,10 @@ 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, - private paymentMethodWarningService: PaymentMethodWarningService, private organizationService: InternalOrganizationServiceAbstraction, private accountService: AccountService, ) {} @@ -252,25 +237,6 @@ export class AppComponent implements OnDestroy, OnInit { new DisableSendPolicy(), new SendOptionsPolicy(), ]); - - combineLatest([ - this.configService.getFeatureFlag$(FeatureFlag.ShowPaymentMethodWarningBanners), - this.paymentMethodWarningsRefresh$, - ]) - .pipe( - filter(([showPaymentMethodWarningBanners]) => showPaymentMethodWarningBanners), - switchMap(() => this.organizationService.memberOrganizations$), - switchMap( - async (organizations) => - await Promise.all( - organizations.map((organization) => - this.paymentMethodWarningService.update(organization.id), - ), - ), - ), - takeUntil(this.destroy$), - ) - .subscribe(); } ngOnDestroy() { @@ -323,13 +289,11 @@ export class AppComponent implements OnDestroy, OnInit { ); await Promise.all([ - this.syncService.setLastSync(new Date(0)), this.cryptoService.clearKeys(), this.cipherService.clear(userId), this.folderService.clear(userId), this.collectionService.clear(userId), this.biometricStateService.logout(userId), - this.paymentMethodWarningService.clear(), ]); await this.stateEventRunnerService.handleEvent("logout", userId); diff --git a/apps/web/src/app/auth/core/services/index.ts b/apps/web/src/app/auth/core/services/index.ts index 4ef20f4b97d..c85f0f3204c 100644 --- a/apps/web/src/app/auth/core/services/index.ts +++ b/apps/web/src/app/auth/core/services/index.ts @@ -1 +1,3 @@ export * from "./webauthn-login"; +export * from "./set-password-jit"; +export * from "./registration"; diff --git a/apps/web/src/app/auth/core/services/registration/index.ts b/apps/web/src/app/auth/core/services/registration/index.ts new file mode 100644 index 00000000000..6d5565e7e30 --- /dev/null +++ b/apps/web/src/app/auth/core/services/registration/index.ts @@ -0,0 +1 @@ +export * from "./web-registration-finish.service"; diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts new file mode 100644 index 00000000000..2faf3f85d10 --- /dev/null +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts @@ -0,0 +1,243 @@ +import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { PasswordInputResult } from "@bitwarden/auth/angular"; +import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; +import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { MasterKey, UserKey } from "@bitwarden/common/types/key"; + +import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service"; +import { OrganizationInvite } from "../../../organization-invite/organization-invite"; + +import { WebRegistrationFinishService } from "./web-registration-finish.service"; + +describe("DefaultRegistrationFinishService", () => { + let service: WebRegistrationFinishService; + + let cryptoService: MockProxy; + let accountApiService: MockProxy; + let acceptOrgInviteService: MockProxy; + let policyApiService: MockProxy; + let logService: MockProxy; + let policyService: MockProxy; + + beforeEach(() => { + cryptoService = mock(); + accountApiService = mock(); + acceptOrgInviteService = mock(); + policyApiService = mock(); + logService = mock(); + policyService = mock(); + + service = new WebRegistrationFinishService( + cryptoService, + accountApiService, + acceptOrgInviteService, + policyApiService, + logService, + policyService, + ); + }); + + it("instantiates", () => { + expect(service).not.toBeFalsy(); + }); + + describe("getMasterPasswordPolicyOptsFromOrgInvite()", () => { + let orgInvite: OrganizationInvite | null; + + beforeEach(() => { + orgInvite = new OrganizationInvite(); + orgInvite.organizationId = "organizationId"; + orgInvite.organizationUserId = "organizationUserId"; + orgInvite.token = "orgInviteToken"; + orgInvite.email = "email"; + }); + + it("returns null when the org invite is null", async () => { + acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null); + + const result = await service.getMasterPasswordPolicyOptsFromOrgInvite(); + + expect(result).toBeNull(); + expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled(); + }); + + it("returns null when the policies are null", async () => { + acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); + policyApiService.getPoliciesByToken.mockResolvedValue(null); + + const result = await service.getMasterPasswordPolicyOptsFromOrgInvite(); + + expect(result).toBeNull(); + expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled(); + expect(policyApiService.getPoliciesByToken).toHaveBeenCalledWith( + orgInvite.organizationId, + orgInvite.token, + orgInvite.email, + orgInvite.organizationUserId, + ); + }); + + it("logs an error and returns null when policies cannot be fetched", async () => { + acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); + policyApiService.getPoliciesByToken.mockRejectedValue(new Error("error")); + + const result = await service.getMasterPasswordPolicyOptsFromOrgInvite(); + + expect(result).toBeNull(); + expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled(); + expect(policyApiService.getPoliciesByToken).toHaveBeenCalledWith( + orgInvite.organizationId, + orgInvite.token, + orgInvite.email, + orgInvite.organizationUserId, + ); + expect(logService.error).toHaveBeenCalled(); + }); + + it("returns the master password policy options from the organization invite when it exists", async () => { + const masterPasswordPolicies = [new Policy()]; + const masterPasswordPolicyOptions = new MasterPasswordPolicyOptions(); + + acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); + policyApiService.getPoliciesByToken.mockResolvedValue(masterPasswordPolicies); + policyService.masterPasswordPolicyOptions$.mockReturnValue(of(masterPasswordPolicyOptions)); + + const result = await service.getMasterPasswordPolicyOptsFromOrgInvite(); + + expect(result).toEqual(masterPasswordPolicyOptions); + expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled(); + expect(policyApiService.getPoliciesByToken).toHaveBeenCalledWith( + orgInvite.organizationId, + orgInvite.token, + orgInvite.email, + orgInvite.organizationUserId, + ); + }); + }); + + describe("finishRegistration()", () => { + let email: string; + let emailVerificationToken: string; + let masterKey: MasterKey; + let passwordInputResult: PasswordInputResult; + let userKey: UserKey; + let userKeyEncString: EncString; + let userKeyPair: [string, EncString]; + let capchaBypassToken: string; + + let orgInvite: OrganizationInvite; + + beforeEach(() => { + email = "test@email.com"; + emailVerificationToken = "emailVerificationToken"; + masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey; + passwordInputResult = { + masterKey: masterKey, + masterKeyHash: "masterKeyHash", + localMasterKeyHash: "localMasterKeyHash", + kdfConfig: DEFAULT_KDF_CONFIG, + hint: "hint", + password: "password", + }; + + userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; + userKeyEncString = new EncString("userKeyEncrypted"); + + userKeyPair = ["publicKey", new EncString("privateKey")]; + capchaBypassToken = "capchaBypassToken"; + + orgInvite = new OrganizationInvite(); + orgInvite.organizationUserId = "organizationUserId"; + orgInvite.token = "orgInviteToken"; + }); + + it("throws an error if the user key cannot be created", async () => { + cryptoService.makeUserKey.mockResolvedValue([null, null]); + + await expect(service.finishRegistration(email, passwordInputResult)).rejects.toThrow( + "User key could not be created", + ); + }); + + it("registers the user and returns a captcha bypass token when given valid email verification input", async () => { + cryptoService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); + cryptoService.makeKeyPair.mockResolvedValue(userKeyPair); + accountApiService.registerFinish.mockResolvedValue(capchaBypassToken); + acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null); + + const result = await service.finishRegistration( + email, + passwordInputResult, + emailVerificationToken, + ); + + expect(result).toEqual(capchaBypassToken); + + expect(cryptoService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(cryptoService.makeKeyPair).toHaveBeenCalledWith(userKey); + expect(accountApiService.registerFinish).toHaveBeenCalledWith( + expect.objectContaining({ + email, + emailVerificationToken: emailVerificationToken, + masterPasswordHash: passwordInputResult.masterKeyHash, + masterPasswordHint: passwordInputResult.hint, + userSymmetricKey: userKeyEncString.encryptedString, + userAsymmetricKeys: { + publicKey: userKeyPair[0], + encryptedPrivateKey: userKeyPair[1].encryptedString, + }, + kdf: passwordInputResult.kdfConfig.kdfType, + kdfIterations: passwordInputResult.kdfConfig.iterations, + kdfMemory: undefined, + kdfParallelism: undefined, + orgInviteToken: undefined, + organizationUserId: undefined, + }), + ); + }); + + it("it registers the user and returns a captcha bypass token when given an org invite", async () => { + cryptoService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); + cryptoService.makeKeyPair.mockResolvedValue(userKeyPair); + accountApiService.registerFinish.mockResolvedValue(capchaBypassToken); + acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); + + const result = await service.finishRegistration(email, passwordInputResult); + + expect(result).toEqual(capchaBypassToken); + + expect(cryptoService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(cryptoService.makeKeyPair).toHaveBeenCalledWith(userKey); + expect(accountApiService.registerFinish).toHaveBeenCalledWith( + expect.objectContaining({ + email, + emailVerificationToken: undefined, + masterPasswordHash: passwordInputResult.masterKeyHash, + masterPasswordHint: passwordInputResult.hint, + userSymmetricKey: userKeyEncString.encryptedString, + userAsymmetricKeys: { + publicKey: userKeyPair[0], + encryptedPrivateKey: userKeyPair[1].encryptedString, + }, + kdf: passwordInputResult.kdfConfig.kdfType, + kdfIterations: passwordInputResult.kdfConfig.iterations, + kdfMemory: undefined, + kdfParallelism: undefined, + orgInviteToken: orgInvite.token, + organizationUserId: orgInvite.organizationUserId, + }), + ); + }); + }); +}); diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts new file mode 100644 index 00000000000..5239601bbcd --- /dev/null +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts @@ -0,0 +1,106 @@ +import { firstValueFrom } from "rxjs"; + +import { + DefaultRegistrationFinishService, + PasswordInputResult, + RegistrationFinishService, +} from "@bitwarden/auth/angular"; +import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; +import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; + +import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service"; + +export class WebRegistrationFinishService + extends DefaultRegistrationFinishService + implements RegistrationFinishService +{ + constructor( + protected cryptoService: CryptoService, + protected accountApiService: AccountApiService, + private acceptOrgInviteService: AcceptOrganizationInviteService, + private policyApiService: PolicyApiServiceAbstraction, + private logService: LogService, + private policyService: PolicyService, + ) { + super(cryptoService, accountApiService); + } + + override async getMasterPasswordPolicyOptsFromOrgInvite(): Promise { + // If there's a deep linked org invite, use it to get the password policies + const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite(); + + if (orgInvite == null) { + return null; + } + + let policies: Policy[] | null = null; + try { + policies = await this.policyApiService.getPoliciesByToken( + orgInvite.organizationId, + orgInvite.token, + orgInvite.email, + orgInvite.organizationUserId, + ); + } catch (e) { + this.logService.error(e); + } + + if (policies == null) { + return null; + } + + const masterPasswordPolicyOpts: MasterPasswordPolicyOptions = await firstValueFrom( + this.policyService.masterPasswordPolicyOptions$(policies), + ); + + return masterPasswordPolicyOpts; + } + + // Note: the org invite token and email verification are mutually exclusive. Only one will be present. + override async buildRegisterRequest( + email: string, + passwordInputResult: PasswordInputResult, + encryptedUserKey: EncryptedString, + userAsymmetricKeys: [string, EncString], + emailVerificationToken?: string, + orgSponsoredFreeFamilyPlanToken?: string, + acceptEmergencyAccessInviteToken?: string, + emergencyAccessId?: string, + ): Promise { + const registerRequest = await super.buildRegisterRequest( + email, + passwordInputResult, + encryptedUserKey, + userAsymmetricKeys, + emailVerificationToken, + ); + + // web specific logic + // Org invites are deep linked. Non-existent accounts are redirected to the register page. + // Org user id and token are included here only for validation and two factor purposes. + const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite(); + if (orgInvite != null) { + registerRequest.organizationUserId = orgInvite.organizationUserId; + registerRequest.orgInviteToken = orgInvite.token; + } + // Invite is accepted after login (on deep link redirect). + + if (orgSponsoredFreeFamilyPlanToken) { + registerRequest.orgSponsoredFreeFamilyPlanToken = orgSponsoredFreeFamilyPlanToken; + } + + if (acceptEmergencyAccessInviteToken && emergencyAccessId) { + registerRequest.acceptEmergencyAccessInviteToken = acceptEmergencyAccessInviteToken; + registerRequest.acceptEmergencyAccessId = emergencyAccessId; + } + + return registerRequest; + } +} diff --git a/apps/web/src/app/auth/core/services/set-password-jit/index.ts b/apps/web/src/app/auth/core/services/set-password-jit/index.ts new file mode 100644 index 00000000000..fc119fd964f --- /dev/null +++ b/apps/web/src/app/auth/core/services/set-password-jit/index.ts @@ -0,0 +1 @@ +export * from "./web-set-password-jit.service"; diff --git a/apps/web/src/app/auth/core/services/set-password-jit/web-set-password-jit.service.ts b/apps/web/src/app/auth/core/services/set-password-jit/web-set-password-jit.service.ts new file mode 100644 index 00000000000..62175f1256d --- /dev/null +++ b/apps/web/src/app/auth/core/services/set-password-jit/web-set-password-jit.service.ts @@ -0,0 +1,27 @@ +import { inject } from "@angular/core"; + +import { + DefaultSetPasswordJitService, + SetPasswordCredentials, + SetPasswordJitService, +} from "@bitwarden/auth/angular"; + +import { RouterService } from "../../../../core/router.service"; +import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service"; + +export class WebSetPasswordJitService + extends DefaultSetPasswordJitService + implements SetPasswordJitService +{ + routerService = inject(RouterService); + acceptOrganizationInviteService = inject(AcceptOrganizationInviteService); + + override async setPassword(credentials: SetPasswordCredentials) { + await super.setPassword(credentials); + + // SSO JIT accepts org invites when setting their MP, meaning + // we can clear the deep linked url for accepting it. + await this.routerService.getAndClearLoginRedirectUrl(); + await this.acceptOrganizationInviteService.clearOrganizationInvitation(); + } +} diff --git a/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.html b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.html index 11491bd5560..3fa795db157 100644 --- a/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.html +++ b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.html @@ -26,14 +26,8 @@ > {{ "logIn" | i18n }} - + diff --git a/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts index 378726b8407..cd11bc72f37 100644 --- a/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts +++ b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts @@ -1,8 +1,9 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; +import { RegisterRouteService } from "@bitwarden/auth/common"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -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"; @@ -18,6 +19,8 @@ import { EmergencyAccessService } from "../services/emergency-access.service"; }) export class AcceptEmergencyComponent extends BaseAcceptComponent { name: string; + emergencyAccessId: string; + acceptEmergencyAccessInviteToken: string; protected requiredParameters: string[] = ["id", "name", "email", "token"]; protected failedShortMessage = "emergencyInviteAcceptFailedShort"; @@ -29,10 +32,10 @@ export class AcceptEmergencyComponent extends BaseAcceptComponent { i18nService: I18nService, route: ActivatedRoute, authService: AuthService, - configService: ConfigService, + registerRouteService: RegisterRouteService, private emergencyAccessService: EmergencyAccessService, ) { - super(router, platformUtilsService, i18nService, route, authService, configService); + super(router, platformUtilsService, i18nService, route, authService, registerRouteService); } async authedHandler(qParams: Params): Promise { @@ -55,5 +58,36 @@ export class AcceptEmergencyComponent extends BaseAcceptComponent { // Fix URL encoding of space issue with Angular this.name = this.name.replace(/\+/g, " "); } + + if (qParams.id) { + this.emergencyAccessId = qParams.id; + } + + if (qParams.token) { + this.acceptEmergencyAccessInviteToken = qParams.token; + } + } + + async register() { + let queryParams: Params; + let registerRoute = await firstValueFrom(this.registerRoute$); + if (registerRoute === "/register") { + queryParams = { + email: this.email, + }; + } else if (registerRoute === "/signup") { + // We have to override the base component route as we don't need users to + // complete email verification if they are coming directly an emailed invite. + registerRoute = "/finish-signup"; + queryParams = { + email: this.email, + acceptEmergencyAccessInviteToken: this.acceptEmergencyAccessInviteToken, + emergencyAccessId: this.emergencyAccessId, + }; + } + + await this.router.navigate([registerRoute], { + queryParams: queryParams, + }); } } diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts index 7906731d81e..6f68821943b 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.spec.ts @@ -4,6 +4,8 @@ import mock from "jest-mock-extended/lib/Mock"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { UserKeyResponse } from "@bitwarden/common/models/response/user-key.response"; +import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -31,15 +33,18 @@ describe("EmergencyAccessService", () => { let apiService: MockProxy; let cryptoService: MockProxy; let encryptService: MockProxy; + let bulkEncryptService: MockProxy; let cipherService: MockProxy; let logService: MockProxy; let emergencyAccessService: EmergencyAccessService; + let configService: ConfigService; beforeAll(() => { emergencyAccessApiService = mock(); apiService = mock(); cryptoService = mock(); encryptService = mock(); + bulkEncryptService = mock(); cipherService = mock(); logService = mock(); @@ -48,8 +53,10 @@ describe("EmergencyAccessService", () => { apiService, cryptoService, encryptService, + bulkEncryptService, cipherService, logService, + configService, ); }); diff --git a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts index 362b1dec3cc..5b9d73c75e5 100644 --- a/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts +++ b/apps/web/src/app/auth/emergency-access/services/emergency-access.service.ts @@ -9,6 +9,9 @@ import { KdfConfig, PBKDF2KdfConfig, } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -45,8 +48,10 @@ export class EmergencyAccessService private apiService: ApiService, private cryptoService: CryptoService, private encryptService: EncryptService, + private bulkEncryptService: BulkEncryptService, private cipherService: CipherService, private logService: LogService, + private configService: ConfigService, ) {} /** @@ -225,10 +230,18 @@ export class EmergencyAccessService ); const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey; - const ciphers = await this.encryptService.decryptItems( - response.ciphers.map((c) => new Cipher(c)), - grantorUserKey, - ); + let ciphers: CipherView[] = []; + if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) { + ciphers = await this.bulkEncryptService.decryptItems( + response.ciphers.map((c) => new Cipher(c)), + grantorUserKey, + ); + } else { + ciphers = await this.encryptService.decryptItems( + response.ciphers.map((c) => new Cipher(c)), + grantorUserKey, + ); + } return ciphers.sort(this.cipherService.getLocaleSortingFunction()); } diff --git a/apps/web/src/app/auth/hint.component.ts b/apps/web/src/app/auth/hint.component.ts index 91e9ca5cebb..753bdb342f9 100644 --- a/apps/web/src/app/auth/hint.component.ts +++ b/apps/web/src/app/auth/hint.component.ts @@ -1,4 +1,4 @@ -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { Router } from "@angular/router"; @@ -8,12 +8,13 @@ import { ApiService } from "@bitwarden/common/abstractions/api.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 { ToastService } from "@bitwarden/components"; @Component({ selector: "app-hint", templateUrl: "hint.component.html", }) -export class HintComponent extends BaseHintComponent { +export class HintComponent extends BaseHintComponent implements OnInit { formGroup = this.formBuilder.group({ email: ["", [Validators.email, Validators.required]], }); @@ -30,12 +31,21 @@ export class HintComponent extends BaseHintComponent { logService: LogService, loginEmailService: LoginEmailServiceAbstraction, private formBuilder: FormBuilder, + protected toastService: ToastService, ) { - super(router, i18nService, apiService, platformUtilsService, logService, loginEmailService); + super( + router, + i18nService, + apiService, + platformUtilsService, + logService, + loginEmailService, + toastService, + ); } - ngOnInit(): void { - super.ngOnInit(); + async ngOnInit(): Promise { + await super.ngOnInit(); this.emailFormControl.setValue(this.email); } diff --git a/apps/web/src/app/auth/key-rotation/request/update-key.request.ts b/apps/web/src/app/auth/key-rotation/request/update-key.request.ts index 9ea40c88e6e..0988ed54a99 100644 --- a/apps/web/src/app/auth/key-rotation/request/update-key.request.ts +++ b/apps/web/src/app/auth/key-rotation/request/update-key.request.ts @@ -1,4 +1,4 @@ -import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; +import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common"; import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request"; import { SendWithIdRequest } from "@bitwarden/common/src/tools/send/models/request/send-with-id.request"; import { CipherWithIdRequest } from "@bitwarden/common/src/vault/models/request/cipher-with-id.request"; diff --git a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts index a9727532051..2c803a627f3 100644 --- a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts +++ b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts @@ -1,7 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; -import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; +import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/admin-console/common"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request"; diff --git a/apps/web/src/app/auth/lock.component.html b/apps/web/src/app/auth/lock.component.html index 2cdc4d2a651..f630906223b 100644 --- a/apps/web/src/app/auth/lock.component.html +++ b/apps/web/src/app/auth/lock.component.html @@ -1,66 +1,27 @@ -
    -
    -
    -

    - -

    -

    {{ "yourVaultIsLocked" | i18n }}

    -
    -
    -
    - -
    - - -
    - - {{ "loggedInAsEmailOn" | i18n: email : webVaultHostname }} - -
    -
    -
    - - -
    -
    -
    -
    + + + {{ "masterPass" | i18n }} + + + {{ "loggedInAsEmailOn" | i18n: email : webVaultHostname }} + + +
    + +
    + +
    diff --git a/apps/web/src/app/auth/lock.component.ts b/apps/web/src/app/auth/lock.component.ts index 021bf0f9df4..b83723bca47 100644 --- a/apps/web/src/app/auth/lock.component.ts +++ b/apps/web/src/app/auth/lock.component.ts @@ -1,18 +1,49 @@ -import { Component } from "@angular/core"; +import { Component, OnInit, inject } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component"; +import { SharedModule } from "../shared"; + @Component({ selector: "app-lock", templateUrl: "lock.component.html", + standalone: true, + imports: [SharedModule], }) -export class LockComponent extends BaseLockComponent { +export class LockComponent extends BaseLockComponent implements OnInit { + formBuilder = inject(FormBuilder); + + formGroup = this.formBuilder.group({ + masterPassword: ["", { validators: Validators.required, updateOn: "submit" }], + }); + + get masterPasswordFormControl() { + return this.formGroup.controls.masterPassword; + } + async ngOnInit() { await super.ngOnInit(); + + this.masterPasswordFormControl.setValue(this.masterPassword); + this.onSuccessfulSubmit = async () => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigateByUrl(this.successRoute); + await this.router.navigateByUrl(this.successRoute); }; } + + async superSubmit() { + await super.submit(); + } + + submit = async () => { + this.formGroup.markAllAsTouched(); + + if (this.formGroup.invalid) { + return; + } + + this.masterPassword = this.masterPasswordFormControl.value; + await this.superSubmit(); + }; } diff --git a/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.html b/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.html index ed59cc12388..615edb82d0c 100644 --- a/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.html +++ b/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.html @@ -13,7 +13,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}

    diff --git a/apps/web/src/app/auth/login/login.component.html b/apps/web/src/app/auth/login/login.component.html index f54153be6ae..64f1e80a990 100644 --- a/apps/web/src/app/auth/login/login.component.html +++ b/apps/web/src/app/auth/login/login.component.html @@ -56,7 +56,7 @@ clicking on the link. Mousedown fires before onBlur. --> {{ "createAccount" | i18n }}{{ "getMasterPasswordHint" | i18n }}
    diff --git a/apps/web/src/app/auth/login/login.component.ts b/apps/web/src/app/auth/login/login.component.ts index d7d34a4c2fd..1422a7c1239 100644 --- a/apps/web/src/app/auth/login/login.component.ts +++ b/apps/web/src/app/auth/login/login.component.ts @@ -1,7 +1,7 @@ import { Component, NgZone, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { takeUntil } from "rxjs"; +import { firstValueFrom, takeUntil } from "rxjs"; import { first } from "rxjs/operators"; import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component"; @@ -9,6 +9,7 @@ import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstrac import { LoginStrategyServiceAbstraction, LoginEmailServiceAbstraction, + RegisterRouteService, } from "@bitwarden/auth/common"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; @@ -20,17 +21,19 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/ import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; +import { ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { flagEnabled } from "../../../utils/flags"; -import { RouterService, StateService } from "../../core"; +import { RouterService } from "../../core"; import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service"; import { OrganizationInvite } from "../organization-invite/organization-invite"; @@ -68,7 +71,8 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { loginEmailService: LoginEmailServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction, - configService: ConfigService, + registerRouteService: RegisterRouteService, + toastService: ToastService, ) { super( devicesApiService, @@ -89,7 +93,8 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { loginEmailService, ssoLoginService, webAuthnLoginService, - configService, + registerRouteService, + toastService, ); this.onSuccessfulLoginNavigate = this.goAfterLogIn; this.showPasswordless = flagEnabled("showPasswordless"); @@ -128,7 +133,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { } } - async goAfterLogIn() { + async goAfterLogIn(userId: UserId) { const masterPassword = this.formGroup.value.masterPassword; // Check master password against policy @@ -149,7 +154,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { ) { const policiesData: { [id: string]: PolicyData } = {}; this.policies.map((p) => (policiesData[p.id] = PolicyData.fromPolicy(p))); - await this.policyService.replace(policiesData); + await this.policyService.replace(policiesData, userId); await this.router.navigate(["update-password"]); return; } @@ -160,19 +165,22 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { } async goToHint() { - this.setLoginEmailValues(); + await this.saveEmailSettings(); await this.router.navigateByUrl("/hint"); } async goToRegister() { + // TODO: remove when email verification flag is removed + const registerRoute = await firstValueFrom(this.registerRoute$); + if (this.emailFormControl.valid) { - await this.router.navigate([this.registerRoute], { + await this.router.navigate([registerRoute], { queryParams: { email: this.emailFormControl.value }, }); return; } - await this.router.navigate([this.registerRoute]); + await this.router.navigate([registerRoute]); } protected override async handleMigrateEncryptionKey(result: AuthResult): Promise { diff --git a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts index 6695039307e..6c894f4fa85 100644 --- a/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts +++ b/apps/web/src/app/auth/migrate-encryption/migrate-legacy-encryption.component.ts @@ -7,8 +7,9 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { DialogService, ToastService } from "@bitwarden/components"; import { SharedModule } from "../../shared"; import { UserKeyRotationModule } from "../key-rotation/user-key-rotation.module"; @@ -30,11 +31,13 @@ export class MigrateFromLegacyEncryptionComponent { private accountService: AccountService, private keyRotationService: UserKeyRotationService, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, private cryptoService: CryptoService, private messagingService: MessagingService, private logService: LogService, private syncService: SyncService, + private toastService: ToastService, + private dialogService: DialogService, + private folderApiService: FolderApiServiceAbstraction, ) {} submit = async () => { @@ -59,14 +62,31 @@ export class MigrateFromLegacyEncryptionComponent { await this.keyRotationService.rotateUserKeyAndEncryptedData(masterPassword, activeUser); - this.platformUtilsService.showToast( - "success", - this.i18nService.t("keyUpdated"), - this.i18nService.t("logBackInOthersToo"), - { timeout: 15000 }, - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("keyUpdated"), + message: this.i18nService.t("logBackInOthersToo"), + timeout: 15000, + }); this.messagingService.send("logout"); } catch (e) { + // If the error is due to missing folders, we can delete all folders and try again + if (e.message === "All existing folders must be included in the rotation.") { + const deleteFolders = await this.dialogService.openSimpleDialog({ + type: "warning", + title: { key: "encryptionKeyUpdateCannotProceed" }, + content: { key: "keyUpdateFoldersFailed" }, + acceptButtonText: { key: "ok" }, + cancelButtonText: { key: "cancel" }, + }); + + if (deleteFolders) { + await this.folderApiService.deleteAll(); + await this.syncService.fullSync(true, true); + await this.submit(); + return; + } + } this.logService.error(e); throw e; } diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.component.html b/apps/web/src/app/auth/organization-invite/accept-organization.component.html index 6e88d03ffae..88eaa37e8d2 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.component.html +++ b/apps/web/src/app/auth/organization-invite/accept-organization.component.html @@ -7,7 +7,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }}

    @@ -32,7 +32,7 @@ {{ "logIn" | i18n }} diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.component.ts b/apps/web/src/app/auth/organization-invite/accept-organization.component.ts index 7326c5a5b56..82f24974e25 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.component.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.component.ts @@ -1,8 +1,9 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; +import { RegisterRouteService } from "@bitwarden/auth/common"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -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"; @@ -24,10 +25,10 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent { i18nService: I18nService, route: ActivatedRoute, authService: AuthService, - configService: ConfigService, + registerRouteService: RegisterRouteService, private acceptOrganizationInviteService: AcceptOrganizationInviteService, ) { - super(router, platformUtilsService, i18nService, route, authService, configService); + super(router, platformUtilsService, i18nService, route, authService, registerRouteService); } async authedHandler(qParams: Params): Promise { @@ -91,22 +92,22 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent { // TODO: update logic when email verification flag is removed let queryParams: Params; - if (this.registerRoute === "/register") { + let registerRoute = await firstValueFrom(this.registerRoute$); + if (registerRoute === "/register") { queryParams = { fromOrgInvite: "true", email: invite.email, }; - } else if (this.registerRoute === "/signup") { - // We have to override the base component route b/c it is correct for other components - // that extend the base accept comp. We don't need users to complete email verification + } else if (registerRoute === "/signup") { + // We have to override the base component route as we don't need users to complete email verification // if they are coming directly from an emailed org invite. - this.registerRoute = "/finish-signup"; + registerRoute = "/finish-signup"; queryParams = { email: invite.email, }; } - await this.router.navigate([this.registerRoute], { + await this.router.navigate([registerRoute], { queryParams: queryParams, }); return; diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts index 97a17a5997f..13b704b5466 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.spec.ts @@ -1,9 +1,9 @@ import { FakeGlobalStateProvider } from "@bitwarden/common/../spec/fake-state-provider"; import { MockProxy, mock } from "jest-mock-extended"; +import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -35,7 +35,7 @@ describe("AcceptOrganizationInviteService", () => { let policyService: MockProxy; let logService: MockProxy; let organizationApiService: MockProxy; - let organizationUserService: MockProxy; + let organizationUserApiService: MockProxy; let i18nService: MockProxy; let globalStateProvider: FakeGlobalStateProvider; let globalState: FakeGlobalState; @@ -49,7 +49,7 @@ describe("AcceptOrganizationInviteService", () => { policyService = mock(); logService = mock(); organizationApiService = mock(); - organizationUserService = mock(); + organizationUserApiService = mock(); i18nService = mock(); globalStateProvider = new FakeGlobalStateProvider(); globalState = globalStateProvider.getFake(ORGANIZATION_INVITE); @@ -63,7 +63,7 @@ describe("AcceptOrganizationInviteService", () => { policyService, logService, organizationApiService, - organizationUserService, + organizationUserApiService, i18nService, globalStateProvider, ); @@ -85,10 +85,10 @@ describe("AcceptOrganizationInviteService", () => { const result = await sut.validateAndAcceptInvite(invite); expect(result).toBe(true); - expect(organizationUserService.postOrganizationUserAcceptInit).toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAcceptInit).toHaveBeenCalled(); expect(apiService.refreshIdentityToken).toHaveBeenCalled(); expect(globalState.nextMock).toHaveBeenCalledWith(null); - expect(organizationUserService.postOrganizationUserAccept).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAccept).not.toHaveBeenCalled(); expect(authService.logOut).not.toHaveBeenCalled(); }); @@ -133,10 +133,10 @@ describe("AcceptOrganizationInviteService", () => { const result = await sut.validateAndAcceptInvite(invite); expect(result).toBe(true); - expect(organizationUserService.postOrganizationUserAccept).toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAccept).toHaveBeenCalled(); expect(apiService.refreshIdentityToken).toHaveBeenCalled(); expect(globalState.nextMock).toHaveBeenCalledWith(null); - expect(organizationUserService.postOrganizationUserAcceptInit).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAcceptInit).not.toHaveBeenCalled(); expect(authService.logOut).not.toHaveBeenCalled(); }); @@ -161,8 +161,8 @@ describe("AcceptOrganizationInviteService", () => { const result = await sut.validateAndAcceptInvite(invite); expect(result).toBe(true); - expect(organizationUserService.postOrganizationUserAccept).toHaveBeenCalled(); - expect(organizationUserService.postOrganizationUserAcceptInit).not.toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAccept).toHaveBeenCalled(); + expect(organizationUserApiService.postOrganizationUserAcceptInit).not.toHaveBeenCalled(); expect(authService.logOut).not.toHaveBeenCalled(); }); }); diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.service.ts b/apps/web/src/app/auth/organization-invite/accept-organization.service.ts index d1ffa61f6a9..a7798d480fb 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.service.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.service.ts @@ -1,13 +1,13 @@ import { Injectable } from "@angular/core"; import { BehaviorSubject, firstValueFrom, map } from "rxjs"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { + OrganizationUserApiService, OrganizationUserAcceptRequest, OrganizationUserAcceptInitRequest, -} from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; +} from "@bitwarden/admin-console/common"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -58,7 +58,7 @@ export class AcceptOrganizationInviteService { private readonly policyService: PolicyService, private readonly logService: LogService, private readonly organizationApiService: OrganizationApiServiceAbstraction, - private readonly organizationUserService: OrganizationUserService, + private readonly organizationUserApiService: OrganizationUserApiService, private readonly i18nService: I18nService, private readonly globalStateProvider: GlobalStateProvider, ) { @@ -121,7 +121,7 @@ export class AcceptOrganizationInviteService { private async acceptAndInitOrganization(invite: OrganizationInvite): Promise { await this.prepareAcceptAndInitRequest(invite).then((request) => - this.organizationUserService.postOrganizationUserAcceptInit( + this.organizationUserApiService.postOrganizationUserAcceptInit( invite.organizationId, invite.organizationUserId, request, @@ -156,7 +156,7 @@ export class AcceptOrganizationInviteService { private async accept(invite: OrganizationInvite): Promise { await this.prepareAcceptRequest(invite).then((request) => - this.organizationUserService.postOrganizationUserAccept( + this.organizationUserApiService.postOrganizationUserAccept( invite.organizationId, invite.organizationUserId, request, diff --git a/apps/web/src/app/auth/recover-delete.component.ts b/apps/web/src/app/auth/recover-delete.component.ts index 96afd910598..04c3eb1df25 100644 --- a/apps/web/src/app/auth/recover-delete.component.ts +++ b/apps/web/src/app/auth/recover-delete.component.ts @@ -6,6 +6,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { DeleteRecoverRequest } from "@bitwarden/common/models/request/delete-recover.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-recover-delete", @@ -25,6 +26,7 @@ export class RecoverDeleteComponent { private apiService: ApiService, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, + private toastService: ToastService, ) {} submit = async () => { @@ -35,11 +37,11 @@ export class RecoverDeleteComponent { const request = new DeleteRecoverRequest(); request.email = this.email.value.trim().toLowerCase(); await this.apiService.postAccountRecoverDelete(request); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("deleteRecoverEmailSent"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deleteRecoverEmailSent"), + }); await this.router.navigate(["/"]); }; diff --git a/apps/web/src/app/auth/recover-two-factor.component.ts b/apps/web/src/app/auth/recover-two-factor.component.ts index 28296aa89d5..0774a9c777a 100644 --- a/apps/web/src/app/auth/recover-two-factor.component.ts +++ b/apps/web/src/app/auth/recover-two-factor.component.ts @@ -8,6 +8,7 @@ import { TwoFactorRecoveryRequest } from "@bitwarden/common/auth/models/request/ 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"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-recover-two-factor", @@ -27,6 +28,7 @@ export class RecoverTwoFactorComponent { private i18nService: I18nService, private cryptoService: CryptoService, private loginStrategyService: LoginStrategyServiceAbstraction, + private toastService: ToastService, ) {} get email(): string { @@ -53,11 +55,11 @@ export class RecoverTwoFactorComponent { const key = await this.loginStrategyService.makePreloginKey(this.masterPassword, request.email); request.masterPasswordHash = await this.cryptoService.hashMasterKey(this.masterPassword, key); await this.apiService.postTwoFactorRecover(request); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("twoStepRecoverDisabled"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("twoStepRecoverDisabled"), + }); await this.router.navigate(["/"]); }; } diff --git a/apps/web/src/app/auth/register-form/register-form.component.ts b/apps/web/src/app/auth/register-form/register-form.component.ts index ce1255e023a..bf4a3e8203f 100644 --- a/apps/web/src/app/auth/register-form/register-form.component.ts +++ b/apps/web/src/app/auth/register-form/register-form.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from "@angular/core"; +import { Component, Input, OnInit } from "@angular/core"; import { UntypedFormBuilder } from "@angular/forms"; import { Router } from "@angular/router"; @@ -17,7 +17,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service"; @@ -26,7 +26,7 @@ import { AcceptOrganizationInviteService } from "../organization-invite/accept-o selector: "app-register-form", templateUrl: "./register-form.component.html", }) -export class RegisterFormComponent extends BaseRegisterComponent { +export class RegisterFormComponent extends BaseRegisterComponent implements OnInit { @Input() queryParamEmail: string; @Input() queryParamFromOrgInvite: boolean; @Input() enforcedPolicyOptions: MasterPasswordPolicyOptions; @@ -52,6 +52,7 @@ export class RegisterFormComponent extends BaseRegisterComponent { auditService: AuditService, dialogService: DialogService, acceptOrgInviteService: AcceptOrganizationInviteService, + toastService: ToastService, ) { super( formValidationErrorService, @@ -68,6 +69,7 @@ export class RegisterFormComponent extends BaseRegisterComponent { logService, auditService, dialogService, + toastService, ); super.modifyRegisterRequest = async (request: RegisterRequest) => { // Org invites are deep linked. Non-existent accounts are redirected to the register page. @@ -104,11 +106,11 @@ export class RegisterFormComponent extends BaseRegisterComponent { this.enforcedPolicyOptions, ) ) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("masterPasswordPolicyRequirementsNotMet"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"), + }); return; } diff --git a/apps/web/src/app/auth/settings/account/account.component.ts b/apps/web/src/app/auth/settings/account/account.component.ts index bd49eb36afa..6ee76623740 100644 --- a/apps/web/src/app/auth/settings/account/account.component.ts +++ b/apps/web/src/app/auth/settings/account/account.component.ts @@ -1,4 +1,4 @@ -import { Component, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { lastValueFrom } from "rxjs"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -14,7 +14,7 @@ import { DeleteAccountDialogComponent } from "./delete-account-dialog.component" selector: "app-account", templateUrl: "account.component.html", }) -export class AccountComponent { +export class AccountComponent implements OnInit { @ViewChild("deauthorizeSessionsTemplate", { read: ViewContainerRef, static: true }) deauthModalRef: ViewContainerRef; diff --git a/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.html b/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.html index 05fff978b0c..34e9f734fc0 100644 --- a/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.html +++ b/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.html @@ -26,7 +26,7 @@ title="{{ 'customColor' | i18n }}" [ngClass]="{ '!tw-outline-[3px] tw-outline-primary-600 hover:tw-outline-[3px] hover:tw-outline-primary-600': - customColorSelected + customColorSelected, }" class="tw-relative tw-flex tw-h-24 tw-w-24 tw-cursor-pointer tw-place-content-center tw-content-center tw-justify-center tw-rounded-full tw-border tw-border-solid tw-border-secondary-600 tw-outline tw-outline-0 tw-outline-offset-1 hover:tw-outline-1 hover:tw-outline-primary-300 focus:tw-outline-2 focus:tw-outline-primary-600" [style.background-color]="customColor$ | async" diff --git a/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts b/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts index b555faf9a12..e20245bfa00 100644 --- a/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts +++ b/apps/web/src/app/auth/settings/account/change-avatar-dialog.component.ts @@ -15,7 +15,7 @@ import { ProfileResponse } from "@bitwarden/common/models/response/profile.respo import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; type ChangeAvatarDialogData = { profile: ProfileResponse; @@ -55,6 +55,7 @@ export class ChangeAvatarDialogComponent implements OnInit, OnDestroy { private platformUtilsService: PlatformUtilsService, private avatarService: AvatarService, private dialogRef: DialogRef, + private toastService: ToastService, ) { this.profile = data.profile; } @@ -93,9 +94,17 @@ export class ChangeAvatarDialogComponent implements OnInit, OnDestroy { if (Utils.validateHexColor(this.currentSelection) || this.currentSelection == null) { await this.avatarService.setAvatarColor(this.currentSelection); this.dialogRef.close(); - this.platformUtilsService.showToast("success", null, this.i18nService.t("avatarUpdated")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("avatarUpdated"), + }); } else { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); } }; diff --git a/apps/web/src/app/auth/settings/account/change-email.component.ts b/apps/web/src/app/auth/settings/account/change-email.component.ts index e5a3c72337b..ac493357765 100644 --- a/apps/web/src/app/auth/settings/account/change-email.component.ts +++ b/apps/web/src/app/auth/settings/account/change-email.component.ts @@ -12,6 +12,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-change-email", @@ -39,6 +40,7 @@ export class ChangeEmailComponent implements OnInit { private stateService: StateService, private formBuilder: FormBuilder, private kdfConfigService: KdfConfigService, + private toastService: ToastService, ) {} async ngOnInit() { @@ -100,11 +102,11 @@ export class ChangeEmailComponent implements OnInit { try { await this.apiService.postEmail(request); this.reset(); - this.platformUtilsService.showToast( - "success", - this.i18nService.t("emailChanged"), - this.i18nService.t("logBackIn"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("emailChanged"), + message: this.i18nService.t("logBackIn"), + }); this.messagingService.send("logout"); } catch (e) { this.logService.error(e); diff --git a/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts b/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts index 9b9ba7eb795..dcaf38ee29e 100644 --- a/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts +++ b/apps/web/src/app/auth/settings/account/deauthorize-sessions.component.ts @@ -7,6 +7,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-deauthorize-sessions", @@ -23,6 +24,7 @@ export class DeauthorizeSessionsComponent { private userVerificationService: UserVerificationService, private messagingService: MessagingService, private logService: LogService, + private toastService: ToastService, ) {} async submit() { @@ -31,11 +33,11 @@ export class DeauthorizeSessionsComponent { .buildRequest(this.masterPassword) .then((request) => this.apiService.postSecurityStamp(request)); await this.formPromise; - this.platformUtilsService.showToast( - "success", - this.i18nService.t("sessionsDeauthorized"), - this.i18nService.t("logBackIn"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("sessionsDeauthorized"), + message: this.i18nService.t("logBackIn"), + }); this.messagingService.send("logout"); } catch (e) { this.logService.error(e); diff --git a/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts b/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts index b3dd8fbe616..c7c67416e18 100644 --- a/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts +++ b/apps/web/src/app/auth/settings/account/delete-account-dialog.component.ts @@ -7,7 +7,7 @@ import { Verification } from "@bitwarden/common/auth/types/verification"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Component({ templateUrl: "delete-account-dialog.component.html", @@ -24,6 +24,7 @@ export class DeleteAccountDialogComponent { private formBuilder: FormBuilder, private accountApiService: AccountApiService, private dialogRef: DialogRef, + private toastService: ToastService, ) {} submit = async () => { @@ -31,11 +32,11 @@ export class DeleteAccountDialogComponent { const verification = this.deleteForm.get("verification").value; await this.accountApiService.deleteAccount(verification); this.dialogRef.close(); - this.platformUtilsService.showToast( - "success", - this.i18nService.t("accountDeleted"), - this.i18nService.t("accountDeletedDesc"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("accountDeleted"), + message: this.i18nService.t("accountDeletedDesc"), + }); } catch (e) { if (e instanceof ErrorResponse && e.statusCode === 400) { this.invalidSecret = true; diff --git a/apps/web/src/app/auth/settings/account/profile.component.html b/apps/web/src/app/auth/settings/account/profile.component.html index 4464824c63e..93025420b26 100644 --- a/apps/web/src/app/auth/settings/account/profile.component.html +++ b/apps/web/src/app/auth/settings/account/profile.component.html @@ -19,8 +19,8 @@
    -
    - +
    +
    - + a?.id))); + const newLocalKeyHash = await this.cryptoService.hashMasterKey( + this.masterPassword, + newMasterKey, + HashPurpose.LocalAuthorization, + ); + const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey); if (userKey == null) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("invalidMasterPassword"), - ); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("invalidMasterPassword"), + }); return; } @@ -199,7 +215,10 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent { try { if (this.rotateUserKey) { - this.formPromise = this.apiService.postPassword(request).then(() => { + this.formPromise = this.apiService.postPassword(request).then(async () => { + // we need to save this for local masterkey verification during rotation + await this.masterPasswordService.setMasterKeyHash(newLocalKeyHash, userId as UserId); + await this.masterPasswordService.setMasterKey(newMasterKey, userId as UserId); return this.updateKey(); }); } else { @@ -208,14 +227,18 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent { await this.formPromise; - this.platformUtilsService.showToast( - "success", - this.i18nService.t("masterPasswordChanged"), - this.i18nService.t("logBackIn"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("masterPasswordChanged"), + message: this.i18nService.t("logBackIn"), + }); this.messagingService.send("logout"); } catch { - this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); } } diff --git a/apps/web/src/app/auth/settings/emergency-access/attachments/emergency-access-attachments.component.ts b/apps/web/src/app/auth/settings/emergency-access/attachments/emergency-access-attachments.component.ts index 09c9072cf0e..e0a6f6c53d5 100644 --- a/apps/web/src/app/auth/settings/emergency-access/attachments/emergency-access-attachments.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/attachments/emergency-access-attachments.component.ts @@ -2,6 +2,7 @@ import { Component } from "@angular/core"; import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; @@ -11,7 +12,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Component({ selector: "emergency-access-attachments", @@ -32,6 +33,8 @@ export class EmergencyAccessAttachmentsComponent extends BaseAttachmentsComponen fileDownloadService: FileDownloadService, dialogService: DialogService, billingAccountProfileStateService: BillingAccountProfileStateService, + accountService: AccountService, + toastService: ToastService, ) { super( cipherService, @@ -45,6 +48,8 @@ export class EmergencyAccessAttachmentsComponent extends BaseAttachmentsComponen fileDownloadService, dialogService, billingAccountProfileStateService, + accountService, + toastService, ); } diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html index 1e61585e422..293c5051ce9 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.html @@ -23,6 +23,7 @@ linkType="primary" appA11yTitle="{{ 'learnMore' | i18n }}" href="https://bitwarden.com/help/emergency-access/#user-access" + slot="end" > diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts index d99c693e73e..fa5e80c81f5 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access-add-edit.component.ts @@ -5,7 +5,7 @@ import { FormBuilder, Validators } from "@angular/forms"; 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 { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { EmergencyAccessService } from "../../emergency-access"; import { EmergencyAccessType } from "../../emergency-access/enums/emergency-access-type"; @@ -51,6 +51,7 @@ export class EmergencyAccessAddEditComponent implements OnInit { private platformUtilsService: PlatformUtilsService, private logService: LogService, private dialogRef: DialogRef, + private toastService: ToastService, ) {} async ngOnInit() { this.editMode = this.loading = this.params.emergencyAccessId != null; @@ -104,11 +105,14 @@ export class EmergencyAccessAddEditComponent implements OnInit { this.addEditForm.value.waitTime, ); } - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.params.name), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t( + this.editMode ? "editedUserId" : "invitedUsers", + this.params.name, + ), + }); this.dialogRef.close(EmergencyAccessAddEditDialogResult.Saved); } catch (e) { this.logService.error(e); diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts index 05e65405fb7..d8cedd5bd43 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts @@ -10,7 +10,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { EmergencyAccessService } from "../../emergency-access"; import { EmergencyAccessStatusType } from "../../emergency-access/enums/emergency-access-status-type"; @@ -66,6 +66,7 @@ export class EmergencyAccessComponent implements OnInit { protected dialogService: DialogService, billingAccountProfileStateService: BillingAccountProfileStateService, protected organizationManagementPreferencesService: OrganizationManagementPreferencesService, + private toastService: ToastService, ) { this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; } @@ -121,11 +122,11 @@ export class EmergencyAccessComponent implements OnInit { } this.actionPromise = this.emergencyAccessService.reinvite(contact.id); await this.actionPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("hasBeenReinvited", contact.email), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("hasBeenReinvited", contact.email), + }); this.actionPromise = null; } @@ -153,11 +154,11 @@ export class EmergencyAccessComponent implements OnInit { if (result === EmergencyAccessConfirmDialogResult.Confirmed) { await this.emergencyAccessService.confirm(contact.id, contact.granteeId); updateUser(); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(contact)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(contact)), + }); } return; } @@ -166,11 +167,11 @@ export class EmergencyAccessComponent implements OnInit { await this.actionPromise; updateUser(); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(contact)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(contact)), + }); this.actionPromise = null; } @@ -187,11 +188,11 @@ export class EmergencyAccessComponent implements OnInit { try { await this.emergencyAccessService.delete(details.id); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("removedUserId", this.userNamePipe.transform(details)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("removedUserId", this.userNamePipe.transform(details)), + }); if (details instanceof GranteeEmergencyAccess) { this.removeGrantee(details); @@ -221,11 +222,11 @@ export class EmergencyAccessComponent implements OnInit { await this.emergencyAccessService.requestAccess(details.id); details.status = EmergencyAccessStatusType.RecoveryInitiated; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("requestSent", this.userNamePipe.transform(details)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("requestSent", this.userNamePipe.transform(details)), + }); } async approve(details: GranteeEmergencyAccess) { @@ -250,22 +251,22 @@ export class EmergencyAccessComponent implements OnInit { await this.emergencyAccessService.approve(details.id); details.status = EmergencyAccessStatusType.RecoveryApproved; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("emergencyApproved", this.userNamePipe.transform(details)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("emergencyApproved", this.userNamePipe.transform(details)), + }); } async reject(details: GranteeEmergencyAccess) { await this.emergencyAccessService.reject(details.id); details.status = EmergencyAccessStatusType.Confirmed; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("emergencyRejected", this.userNamePipe.transform(details)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("emergencyRejected", this.userNamePipe.transform(details)), + }); } takeover = async (details: GrantorEmergencyAccess) => { @@ -278,11 +279,11 @@ export class EmergencyAccessComponent implements OnInit { }); const result = await lastValueFrom(dialogRef.closed); if (result === EmergencyAccessTakeoverResultType.Done) { - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("passwordResetFor", this.userNamePipe.transform(details)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("passwordResetFor", this.userNamePipe.transform(details)), + }); } }; diff --git a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts index a3d856aa697..26995c7ce09 100644 --- a/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/takeover/emergency-access-takeover.component.ts @@ -15,7 +15,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { KdfType } from "@bitwarden/common/platform/enums"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { EmergencyAccessService } from "../../../emergency-access"; @@ -64,6 +64,7 @@ export class EmergencyAccessTakeoverComponent kdfConfigService: KdfConfigService, masterPasswordService: InternalMasterPasswordServiceAbstraction, accountService: AccountService, + protected toastService: ToastService, ) { super( i18nService, @@ -77,6 +78,7 @@ export class EmergencyAccessTakeoverComponent kdfConfigService, masterPasswordService, accountService, + toastService, ); } @@ -114,11 +116,11 @@ export class EmergencyAccessTakeoverComponent ); } catch (e) { this.logService.error(e); - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("unexpectedError"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("unexpectedError"), + }); } this.dialogRef.close(EmergencyAccessTakeoverResultType.Done); }; diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts index 0c754e262e1..295037ce6b5 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf-confirmation.component.ts @@ -12,6 +12,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { KdfType } from "@bitwarden/common/platform/enums"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-change-kdf-confirmation", @@ -35,6 +36,7 @@ export class ChangeKdfConfirmationComponent { private messagingService: MessagingService, @Inject(DIALOG_DATA) params: { kdf: KdfType; kdfConfig: KdfConfig }, private accountService: AccountService, + private toastService: ToastService, ) { this.kdfConfig = params.kdfConfig; this.masterPassword = null; @@ -46,11 +48,11 @@ export class ChangeKdfConfirmationComponent { } this.loading = true; await this.makeKeyAndSaveAsync(); - this.platformUtilsService.showToast( - "success", - this.i18nService.t("encKeySettingsChanged"), - this.i18nService.t("logBackIn"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("encKeySettingsChanged"), + message: this.i18nService.t("logBackIn"), + }); this.messagingService.send("logout"); this.loading = false; }; diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html index 4442310faca..478cd77eb6c 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.html @@ -22,6 +22,7 @@ target="_blank" rel="noreferrer" appA11yTitle="{{ 'learnMore' | i18n }}" + slot="end" > @@ -57,6 +58,7 @@ target="_blank" rel="noreferrer" appA11yTitle="{{ 'learnMore' | i18n }}" + slot="end" > diff --git a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts index af65acc1d53..45ceaeccd07 100644 --- a/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts +++ b/apps/web/src/app/auth/settings/security/change-kdf/change-kdf.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from "@angular/core"; +import { Component, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder, FormControl, ValidatorFn, Validators } from "@angular/forms"; import { Subject, takeUntil } from "rxjs"; @@ -18,7 +18,7 @@ import { ChangeKdfConfirmationComponent } from "./change-kdf-confirmation.compon selector: "app-change-kdf", templateUrl: "change-kdf.component.html", }) -export class ChangeKdfComponent implements OnInit { +export class ChangeKdfComponent implements OnInit, OnDestroy { kdfConfig: KdfConfig = DEFAULT_KDF_CONFIG; kdfOptions: any[] = []; private destroy$ = new Subject(); diff --git a/apps/web/src/app/auth/settings/security/security.component.ts b/apps/web/src/app/auth/settings/security/security.component.ts index 3237a2e6a28..1df8145a917 100644 --- a/apps/web/src/app/auth/settings/security/security.component.ts +++ b/apps/web/src/app/auth/settings/security/security.component.ts @@ -1,4 +1,4 @@ -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; @@ -6,7 +6,7 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use selector: "app-security", templateUrl: "security.component.html", }) -export class SecurityComponent { +export class SecurityComponent implements OnInit { showChangePassword = true; constructor(private userVerificationService: UserVerificationService) {} diff --git a/apps/web/src/app/auth/settings/two-factor-authenticator.component.html b/apps/web/src/app/auth/settings/two-factor-authenticator.component.html index f7ea36e02d4..c9214d59caa 100644 --- a/apps/web/src/app/auth/settings/two-factor-authenticator.component.html +++ b/apps/web/src/app/auth/settings/two-factor-authenticator.component.html @@ -72,7 +72,14 @@

    -
    + + +

    + {{ "twoStepAuthenticatorQRCanvasError" | i18n }} +

    + + +
    {{ key }}

    @@ -90,7 +97,7 @@ > {{ (enabled ? "disable" : "enable") | i18n }} - diff --git a/apps/web/src/app/auth/settings/two-factor-authenticator.component.ts b/apps/web/src/app/auth/settings/two-factor-authenticator.component.ts index 68706105313..fdd595b7fcd 100644 --- a/apps/web/src/app/auth/settings/two-factor-authenticator.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-authenticator.component.ts @@ -7,14 +7,17 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; +import { DisableTwoFactorAuthenticatorRequest } from "@bitwarden/common/auth/models/request/disable-two-factor-authenticator.request"; import { UpdateTwoFactorAuthenticatorRequest } from "@bitwarden/common/auth/models/request/update-two-factor-authenticator.request"; import { TwoFactorAuthenticatorResponse } from "@bitwarden/common/auth/models/response/two-factor-authenticator.response"; import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { TwoFactorBaseComponent } from "./two-factor-base.component"; @@ -43,9 +46,10 @@ export class TwoFactorAuthenticatorComponent @Output() onChangeStatus = new EventEmitter(); type = TwoFactorProviderType.Authenticator; key: string; - formPromise: Promise; + private userVerificationToken: string; override componentName = "app-two-factor-authenticator"; + qrScriptError = false; private qrScript: HTMLScriptElement; formGroup = this.formBuilder.group({ @@ -63,6 +67,8 @@ export class TwoFactorAuthenticatorComponent logService: LogService, private accountService: AccountService, dialogService: DialogService, + private configService: ConfigService, + protected toastService: ToastService, ) { super( apiService, @@ -71,6 +77,7 @@ export class TwoFactorAuthenticatorComponent logService, userVerificationService, dialogService, + toastService, ); this.qrScript = window.document.createElement("script"); this.qrScript.src = "scripts/qrious.min.js"; @@ -90,7 +97,7 @@ export class TwoFactorAuthenticatorComponent this.formGroup.controls.token.markAsTouched(); } - auth(authResponse: AuthResponse) { + async auth(authResponse: AuthResponse) { super.auth(authResponse); return this.processResponse(authResponse.response); } @@ -100,56 +107,103 @@ export class TwoFactorAuthenticatorComponent return; } if (this.enabled) { - await this.disableAuthentication(this.formPromise); - this.onChangeStatus.emit(this.enabled); - this.close(); + await this.disableMethod(); + this.dialogRef.close(this.enabled); } else { await this.enable(); - this.onChangeStatus.emit(this.enabled); } + this.onChangeStatus.emit(this.enabled); }; - private async disableAuthentication(promise: Promise) { - return super.disable(promise); - } - protected async enable() { const request = await this.buildRequestModel(UpdateTwoFactorAuthenticatorRequest); request.token = this.formGroup.value.token; request.key = this.key; + request.userVerificationToken = this.userVerificationToken; - return super.enable(async () => { - this.formPromise = this.apiService.putTwoFactorAuthenticator(request); - const response = await this.formPromise; - await this.processResponse(response); + const response = await this.apiService.putTwoFactorAuthenticator(request); + await this.processResponse(response); + this.onUpdated.emit(true); + } + + protected override async disableMethod() { + const twoFactorAuthenticatorTokenFeatureFlag = await this.configService.getFeatureFlag( + FeatureFlag.AuthenticatorTwoFactorToken, + ); + if (twoFactorAuthenticatorTokenFeatureFlag === false) { + return super.disableMethod(); + } + + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "disable" }, + content: { key: "twoStepDisableDesc" }, + type: "warning", }); + + if (!confirmed) { + return; + } + + const request = await this.buildRequestModel(DisableTwoFactorAuthenticatorRequest); + request.type = this.type; + request.key = this.key; + request.userVerificationToken = this.userVerificationToken; + await this.apiService.deleteTwoFactorAuthenticator(request); + this.enabled = false; + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("twoStepDisabled"), + }); + this.onUpdated.emit(false); } private async processResponse(response: TwoFactorAuthenticatorResponse) { this.formGroup.get("token").setValue(null); this.enabled = response.enabled; this.key = response.key; + this.userVerificationToken = response.userVerificationToken; + + await this.waitForQRiousToLoadOrError().catch((error) => { + this.logService.error(error); + this.qrScriptError = true; + }); + + await this.createQRCode(); + } + + private async waitForQRiousToLoadOrError(): Promise { + // Check if QRious is already loaded or if there was an error loading it either way don't wait for it to try and load again + if (typeof window.QRious !== "undefined" || this.qrScriptError) { + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + this.qrScript.onload = () => resolve(); + this.qrScript.onerror = () => + reject(new Error(this.i18nService.t("twoStepAuthenticatorQRCanvasError"))); + }); + } + + private async createQRCode() { + if (this.qrScriptError) { + return; + } const email = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.email)), ); - window.setTimeout(() => { - new window.QRious({ - element: document.getElementById("qr"), - value: - "otpauth://totp/Bitwarden:" + - Utils.encodeRFC3986URIComponent(email) + - "?secret=" + - encodeURIComponent(this.key) + - "&issuer=Bitwarden", - size: 160, - }); - }, 100); + new window.QRious({ + element: document.getElementById("qr"), + value: + "otpauth://totp/Bitwarden:" + + Utils.encodeRFC3986URIComponent(email) + + "?secret=" + + encodeURIComponent(this.key) + + "&issuer=Bitwarden", + size: 160, + }); } - close = () => { - this.dialogRef.close(this.enabled); - }; - static open( dialogService: DialogService, config: DialogConfig>, diff --git a/apps/web/src/app/auth/settings/two-factor-base.component.ts b/apps/web/src/app/auth/settings/two-factor-base.component.ts index f909f83e78a..2a6af1df98c 100644 --- a/apps/web/src/app/auth/settings/two-factor-base.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-base.component.ts @@ -10,7 +10,7 @@ import { AuthResponseBase } from "@bitwarden/common/auth/types/auth-response"; 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 { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; @Directive() export abstract class TwoFactorBaseComponent { @@ -33,6 +33,7 @@ export abstract class TwoFactorBaseComponent { protected logService: LogService, protected userVerificationService: UserVerificationService, protected dialogService: DialogService, + protected toastService: ToastService, ) {} protected auth(authResponse: AuthResponseBase) { @@ -48,7 +49,6 @@ export abstract class TwoFactorBaseComponent { this.onUpdated.emit(true); } catch (e) { this.logService.error(e); - throw e; } } @@ -77,7 +77,11 @@ export abstract class TwoFactorBaseComponent { } await promise; this.enabled = false; - this.platformUtilsService.showToast("success", null, this.i18nService.t("twoStepDisabled")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("twoStepDisabled"), + }); this.onUpdated.emit(false); } catch (e) { this.logService.error(e); @@ -103,7 +107,11 @@ export abstract class TwoFactorBaseComponent { await this.apiService.putTwoFactorDisable(request); } this.enabled = false; - this.platformUtilsService.showToast("success", null, this.i18nService.t("twoStepDisabled")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("twoStepDisabled"), + }); this.onUpdated.emit(false); } 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/settings/two-factor-duo.component.ts b/apps/web/src/app/auth/settings/two-factor-duo.component.ts index 7505fe13b39..1a5b5917108 100644 --- a/apps/web/src/app/auth/settings/two-factor-duo.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-duo.component.ts @@ -1,5 +1,5 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, EventEmitter, Inject, Output } from "@angular/core"; +import { Component, EventEmitter, Inject, OnInit, Output } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -11,7 +11,7 @@ import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; 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 { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { TwoFactorBaseComponent } from "./two-factor-base.component"; @@ -19,7 +19,7 @@ import { TwoFactorBaseComponent } from "./two-factor-base.component"; selector: "app-two-factor-duo", templateUrl: "two-factor-duo.component.html", }) -export class TwoFactorDuoComponent extends TwoFactorBaseComponent { +export class TwoFactorDuoComponent extends TwoFactorBaseComponent implements OnInit { @Output() onChangeStatus: EventEmitter = new EventEmitter(); type = TwoFactorProviderType.Duo; @@ -40,6 +40,7 @@ export class TwoFactorDuoComponent extends TwoFactorBaseComponent { dialogService: DialogService, private formBuilder: FormBuilder, private dialogRef: DialogRef, + protected toastService: ToastService, ) { super( apiService, @@ -48,6 +49,7 @@ export class TwoFactorDuoComponent extends TwoFactorBaseComponent { logService, userVerificationService, dialogService, + toastService, ); } diff --git a/apps/web/src/app/auth/settings/two-factor-email.component.ts b/apps/web/src/app/auth/settings/two-factor-email.component.ts index c37a5ecada6..524b00d114f 100644 --- a/apps/web/src/app/auth/settings/two-factor-email.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-email.component.ts @@ -1,5 +1,5 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, EventEmitter, Inject, Output } from "@angular/core"; +import { Component, EventEmitter, Inject, OnInit, Output } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { firstValueFrom, map } from "rxjs"; @@ -14,7 +14,7 @@ import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; 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 { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { TwoFactorBaseComponent } from "./two-factor-base.component"; @@ -23,7 +23,7 @@ import { TwoFactorBaseComponent } from "./two-factor-base.component"; templateUrl: "two-factor-email.component.html", outputs: ["onUpdated"], }) -export class TwoFactorEmailComponent extends TwoFactorBaseComponent { +export class TwoFactorEmailComponent extends TwoFactorBaseComponent implements OnInit { @Output() onChangeStatus: EventEmitter = new EventEmitter(); type = TwoFactorProviderType.Email; sentEmail: string; @@ -45,6 +45,7 @@ export class TwoFactorEmailComponent extends TwoFactorBaseComponent { dialogService: DialogService, private formBuilder: FormBuilder, private dialogRef: DialogRef, + protected toastService: ToastService, ) { super( apiService, @@ -53,6 +54,7 @@ export class TwoFactorEmailComponent extends TwoFactorBaseComponent { logService, userVerificationService, dialogService, + toastService, ); } get token() { diff --git a/apps/web/src/app/auth/settings/two-factor-setup.component.html b/apps/web/src/app/auth/settings/two-factor-setup.component.html index 33265e91f78..3595d9a7dcb 100644 --- a/apps/web/src/app/auth/settings/two-factor-setup.component.html +++ b/apps/web/src/app/auth/settings/two-factor-setup.component.html @@ -39,7 +39,7 @@ title="{{ 'loading' | i18n }}" aria-hidden="true" > - {{ "loading" | i18n }} + {{ "loading" | i18n }} @@ -64,7 +64,7 @@ title="{{ 'enabled' | i18n }}" aria-hidden="true" > - {{ "enabled" | i18n }} + {{ "enabled" | i18n }} diff --git a/apps/web/src/app/auth/settings/two-factor-setup.component.ts b/apps/web/src/app/auth/settings/two-factor-setup.component.ts index 6f80f17bd24..3b8a9edd955 100644 --- a/apps/web/src/app/auth/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-setup.component.ts @@ -160,10 +160,12 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { if (!result) { return; } - const yubiComp = await this.openModal(this.yubikeyModalRef, TwoFactorYubiKeyComponent); - yubiComp.auth(result); - this.twoFactorSetupSubscription = yubiComp.onUpdated - .pipe(first(), takeUntil(this.destroy$)) + const yubiComp: DialogRef = TwoFactorYubiKeyComponent.open( + this.dialogService, + { data: result }, + ); + yubiComp.componentInstance.onUpdated + .pipe(takeUntil(this.destroy$)) .subscribe((enabled: boolean) => { this.updateStatus(enabled, TwoFactorProviderType.Yubikey); }); @@ -218,7 +220,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { this.dialogService, { data: result }, ); - this.twoFactorSetupSubscription = webAuthnComp.componentInstance.onChangeStatus + this.twoFactorSetupSubscription = webAuthnComp.componentInstance.onUpdated .pipe(first(), takeUntil(this.destroy$)) .subscribe((enabled: boolean) => { webAuthnComp.close(); @@ -266,7 +268,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { this.modal.close(); } this.providers.forEach((p) => { - if (p.type === type) { + if (p.type === type && enabled !== undefined) { p.enabled = enabled; } }); diff --git a/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts b/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts index 5e8ea37e930..6dfee920991 100644 --- a/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-webauthn.component.ts @@ -1,5 +1,5 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, EventEmitter, Inject, NgZone, Output } from "@angular/core"; +import { Component, Inject, NgZone } from "@angular/core"; import { FormControl, FormGroup } from "@angular/forms"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -16,7 +16,7 @@ import { AuthResponse } from "@bitwarden/common/auth/types/auth-response"; 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 { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { TwoFactorBaseComponent } from "./two-factor-base.component"; @@ -33,7 +33,6 @@ interface Key { templateUrl: "two-factor-webauthn.component.html", }) export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { - @Output() onChangeStatus = new EventEmitter(); type = TwoFactorProviderType.WebAuthn; name: string; keys: Key[]; @@ -61,6 +60,7 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { logService: LogService, userVerificationService: UserVerificationService, dialogService: DialogService, + toastService: ToastService, ) { super( apiService, @@ -69,6 +69,7 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { logService, userVerificationService, dialogService, + toastService, ); this.auth(data); } @@ -83,34 +84,33 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { // Should never happen. return Promise.reject(); } + return this.enable(); + }; + + protected async enable() { const request = await this.buildRequestModel(UpdateTwoFactorWebAuthnRequest); request.deviceResponse = this.webAuthnResponse; request.id = this.keyIdAvailable; request.name = this.formGroup.value.name; - return this.enableWebAuth(request); - }; - - private enableWebAuth(request: any) { - return super.enable(async () => { - this.formPromise = this.apiService.putTwoFactorWebAuthn(request); - const response = await this.formPromise; - this.processResponse(response); + const response = await this.apiService.putTwoFactorWebAuthn(request); + this.processResponse(response); + this.toastService.showToast({ + title: this.i18nService.t("success"), + message: this.i18nService.t("twoFactorProviderEnabled"), + variant: "success", }); + this.onUpdated.emit(response.enabled); } disable = async () => { - await this.disableWebAuth(); + await this.disableMethod(); if (!this.enabled) { - this.onChangeStatus.emit(this.enabled); + this.onUpdated.emit(this.enabled); this.dialogRef.close(); } }; - private async disableWebAuth() { - return super.disable(this.formPromise); - } - async remove(key: Key) { if (this.keysConfiguredCount <= 1 || key.removePromise != null) { return; @@ -206,7 +206,7 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent { } } this.enabled = response.enabled; - this.onChangeStatus.emit(this.enabled); + this.onUpdated.emit(this.enabled); } static open( diff --git a/apps/web/src/app/auth/settings/two-factor-yubikey.component.html b/apps/web/src/app/auth/settings/two-factor-yubikey.component.html index 514ba2ac96d..098669d7522 100644 --- a/apps/web/src/app/auth/settings/two-factor-yubikey.component.html +++ b/apps/web/src/app/auth/settings/two-factor-yubikey.component.html @@ -1,118 +1,84 @@ -
    diff --git a/apps/web/src/app/billing/individual/user-subscription.component.ts b/apps/web/src/app/billing/individual/user-subscription.component.ts index 9b615f3a690..cca17f6b9cc 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.ts +++ b/apps/web/src/app/billing/individual/user-subscription.component.ts @@ -5,25 +5,29 @@ import { firstValueFrom, lastValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; +import { + AdjustStorageDialogV2Component, + AdjustStorageDialogV2ResultType, +} from "../shared/adjust-storage-dialog/adjust-storage-dialog-v2.component"; import { AdjustStorageDialogResult, openAdjustStorageDialog, -} from "../shared/adjust-storage.component"; +} from "../shared/adjust-storage-dialog/adjust-storage-dialog.component"; import { OffboardingSurveyDialogResultType, openOffboardingSurvey, } from "../shared/offboarding-survey.component"; -import { - UpdateLicenseDialogComponent, - UpdateLicenseDialogResult, -} from "../shared/update-license-dialog.component"; +import { UpdateLicenseDialogComponent } from "../shared/update-license-dialog.component"; +import { UpdateLicenseDialogResult } from "../shared/update-license-types"; @Component({ templateUrl: "user-subscription.component.html", @@ -36,9 +40,17 @@ export class UserSubscriptionComponent implements OnInit { sub: SubscriptionResponse; selfHosted = false; cloudWebVaultUrl: string; + enableTimeThreshold: boolean; cancelPromise: Promise; reinstatePromise: Promise; + protected enableTimeThreshold$ = this.configService.getFeatureFlag$( + FeatureFlag.EnableTimeThreshold, + ); + + protected deprecateStripeSourcesAPI$ = this.configService.getFeatureFlag$( + FeatureFlag.AC2476_DeprecateStripeSourcesAPI, + ); constructor( private apiService: ApiService, @@ -50,6 +62,8 @@ export class UserSubscriptionComponent implements OnInit { private dialogService: DialogService, private environmentService: EnvironmentService, private billingAccountProfileStateService: BillingAccountProfileStateService, + private toastService: ToastService, + private configService: ConfigService, ) { this.selfHosted = platformUtilsService.isSelfHost(); } @@ -57,6 +71,7 @@ export class UserSubscriptionComponent implements OnInit { async ngOnInit() { this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$); await this.load(); + this.enableTimeThreshold = await firstValueFrom(this.enableTimeThreshold$); this.firstLoaded = true; } @@ -96,7 +111,11 @@ export class UserSubscriptionComponent implements OnInit { try { this.reinstatePromise = this.apiService.postReinstatePremium(); await this.reinstatePromise; - this.platformUtilsService.showToast("success", null, this.i18nService.t("reinstated")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("reinstated"), + }); // 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.load(); @@ -147,15 +166,33 @@ export class UserSubscriptionComponent implements OnInit { }; adjustStorage = async (add: boolean) => { - const dialogRef = openAdjustStorageDialog(this.dialogService, { - data: { - storageGbPrice: 4, - add: add, - }, - }); - const result = await lastValueFrom(dialogRef.closed); - if (result === AdjustStorageDialogResult.Adjusted) { - await this.load(); + const deprecateStripeSourcesAPI = await firstValueFrom(this.deprecateStripeSourcesAPI$); + + if (deprecateStripeSourcesAPI) { + const dialogRef = AdjustStorageDialogV2Component.open(this.dialogService, { + data: { + price: 4, + cadence: "year", + type: add ? "Add" : "Remove", + }, + }); + + const result = await lastValueFrom(dialogRef.closed); + + if (result === AdjustStorageDialogV2ResultType.Submitted) { + await this.load(); + } + } else { + const dialogRef = openAdjustStorageDialog(this.dialogService, { + data: { + storageGbPrice: 4, + add: add, + }, + }); + const result = await lastValueFrom(dialogRef.closed); + if (result === AdjustStorageDialogResult.Adjusted) { + await this.load(); + } } }; 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..60d57a3b199 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 }} @@ -54,4 +56,3 @@ {{ "save" | 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..4fb9ae386a1 100644 --- a/apps/web/src/app/billing/organizations/adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/adjust-subscription.component.ts @@ -5,7 +5,7 @@ import { Subject, takeUntil } from "rxjs"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-subscription-update.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; @Component({ selector: "app-adjust-subscription", @@ -18,6 +18,7 @@ export class AdjustSubscription implements OnInit, OnDestroy { @Input() seatPrice = 0; @Input() interval = "year"; @Output() onAdjusted = new EventEmitter(); + private destroy$ = new Subject(); adjustSubscriptionForm = this.formBuilder.group({ @@ -25,46 +26,33 @@ export class AdjustSubscription implements OnInit, OnDestroy { limitSubscription: [false], newMaxSeats: [0, [Validators.min(0)]], }); - get limitSubscription(): boolean { - return this.adjustSubscriptionForm.value.limitSubscription; - } + constructor( private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, private organizationApiService: OrganizationApiServiceAbstraction, private formBuilder: FormBuilder, + private toastService: ToastService, ) {} ngOnInit() { + this.adjustSubscriptionForm.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => { + const maxAutoscaleSeatsControl = this.adjustSubscriptionForm.controls.newMaxSeats; + + if (value.limitSubscription) { + maxAutoscaleSeatsControl.setValidators([Validators.min(value.newSeatCount)]); + maxAutoscaleSeatsControl.enable({ emitEvent: false }); + } else { + maxAutoscaleSeatsControl.disable({ emitEvent: false }); + } + }); + this.adjustSubscriptionForm.patchValue({ newSeatCount: this.currentSeatCount, - limitSubscription: this.maxAutoscaleSeats != null, newMaxSeats: this.maxAutoscaleSeats, + limitSubscription: this.maxAutoscaleSeats != null, }); - this.adjustSubscriptionForm - .get("limitSubscription") - .valueChanges.pipe(takeUntil(this.destroy$)) - .subscribe((value: boolean) => { - if (value) { - this.adjustSubscriptionForm - .get("newMaxSeats") - .addValidators([ - Validators.min( - this.adjustSubscriptionForm.value.newSeatCount == null - ? 1 - : this.adjustSubscriptionForm.value.newSeatCount, - ), - Validators.required, - ]); - } - this.adjustSubscriptionForm.get("newMaxSeats").updateValueAndValidity(); - }); } - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - } submit = async () => { this.adjustSubscriptionForm.markAllAsTouched(); if (this.adjustSubscriptionForm.invalid) { @@ -76,7 +64,11 @@ export class AdjustSubscription implements OnInit, OnDestroy { ); await this.organizationApiService.updatePasswordManagerSeats(this.organizationId, request); - this.platformUtilsService.showToast("success", null, this.i18nService.t("subscriptionUpdated")); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("subscriptionUpdated"), + }); this.onAdjusted.emit(); }; @@ -93,17 +85,20 @@ export class AdjustSubscription implements OnInit, OnDestroy { : 0; } - get additionalMaxSeatCount(): number { - return this.adjustSubscriptionForm.value.newMaxSeats - ? this.adjustSubscriptionForm.value.newMaxSeats - this.currentSeatCount - : 0; - } - - get adjustedSeatTotal(): number { - return this.additionalSeatCount * this.seatPrice; - } - get maxSeatTotal(): number { - return this.additionalMaxSeatCount * this.seatPrice; + return Math.abs((this.adjustSubscriptionForm.value.newMaxSeats ?? 0) * this.seatPrice); + } + + get seatTotalCost(): number { + return Math.abs(this.adjustSubscriptionForm.value.newSeatCount * this.seatPrice); + } + + get limitSubscription(): boolean { + return this.adjustSubscriptionForm.value.limitSubscription; + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); } } diff --git a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html index e1f5431c45b..8b5ef867cca 100644 --- a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html +++ b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html @@ -30,6 +30,8 @@ bitIconButton="bwi-clone" bitSuffix type="button" + showToast + [valueLabel]="'billingSyncKey' | i18n" [appCopyClick]="clientSecret" [appA11yTitle]="'copyValue' | i18n" > diff --git a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.ts b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.ts index 95a29229cf6..deb2c9da3ed 100644 --- a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.ts +++ b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.ts @@ -12,7 +12,7 @@ import { Verification } from "@bitwarden/common/auth/types/verification"; 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 { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; export interface BillingSyncApiModalData { organizationId: string; @@ -43,6 +43,7 @@ export class BillingSyncApiKeyComponent { private i18nService: I18nService, private organizationApiService: OrganizationApiServiceAbstraction, private logService: LogService, + private toastService: ToastService, ) { this.organizationId = data.organizationId; this.hasBillingToken = data.hasBillingToken; @@ -67,11 +68,11 @@ export class BillingSyncApiKeyComponent { }); await this.load(response); this.showRotateScreen = false; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("billingSyncApiKeyRotated"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("billingSyncApiKeyRotated"), + }); } else { const response = await request.then((request) => { return this.organizationApiService.getOrCreateApiKey(this.organizationId, request); diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html new file mode 100644 index 00000000000..766646003ba --- /dev/null +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html @@ -0,0 +1,1002 @@ +
    + + + {{ "upgradeFreeOrganization" | i18n: currentPlanName }} + +
    +

    {{ "upgradePlans" | i18n }}

    +
    + {{ "selectAPlan" | i18n }} + +
    + {{ + "upgradeDiscount" + | i18n + : (selectedInterval === planIntervals.Annually + ? discountPercentageFromSub + this.discountPercentage + : this.discountPercentageFromSub) + }} + +
    + + + {{ planInterval.name }} + + +
    +
    +
    + + +
    +
    +
    +
    + {{ "recommended" | i18n }} +
    +
    +

    + {{ + selectableProduct.nameLocalizationKey | i18n + }} + + {{ "current" | i18n }} +

    + + + + {{ + (selectableProduct.isAnnual + ? selectableProduct.PasswordManager.basePrice / 12 + : selectableProduct.PasswordManager.basePrice + ) | currency: "$" + }} + + /{{ "monthPerMember" | i18n }} + + + {{ ("additionalUsers" | i18n).toLowerCase() }} + {{ + (selectableProduct.isAnnual + ? selectableProduct.PasswordManager.seatPrice / 12 + : selectableProduct.PasswordManager.seatPrice + ) | currency: "$" + }} + /{{ "month" | i18n }} + + + + + + {{ + "costPerMember" + | i18n + : ((selectableProduct.isAnnual + ? selectableProduct.PasswordManager.seatPrice / 12 + : selectableProduct.PasswordManager.seatPrice + ) + | currency: "$") + }} + + /{{ "monthPerMember" | i18n }} + + {{ "freeForever" | i18n }} + +
    +
    + + +

    + {{ "bitwardenPasswordManager" | i18n }} +

    +

    {{ "enterprisePlanUpgradeMessage" | i18n }}

    + +
      +
    • + + {{ "includeEnterprisePolicies" | i18n }} +
    • +
    • + + {{ "passwordLessSso" | i18n }} +
    • +
    • + + {{ "accountRecovery" | i18n }} +
    • +
    • + + {{ "customRoles" | i18n }} +
    • +
    + +

    + {{ "bitwardenSecretsManager" | i18n }} +

    +
      +
    • + + {{ "unlimitedSecretsStorage" | i18n }} +
    • +
    • + + {{ "unlimitedUsers" | i18n }} +
    • +
    • + + {{ "unlimitedProjects" | i18n }} +
    • +
    • + + {{ "UpTo50MachineAccounts" | i18n }} +
    • +
    +
    + + +
      +
    • {{ "includeAllTeamsStarterFeatures" | i18n }}
    • +
    • {{ "chooseMonthlyOrAnnualBilling" | i18n }}
    • +
    • {{ "abilityToAddMoreThanNMembers" | i18n: 10 }}
    • +
    +
    + +

    + {{ "bitwardenPasswordManager" | i18n }} +

    +

    + {{ "teamsPlanUpgradeMessage" | i18n }} +

    +

    + {{ "familyPlanUpgradeMessage" | i18n }} +

    +
      +
    • + + {{ "premiumAccounts" | i18n }} +
    • +
    • + + {{ "unlimitedSharing" | i18n }} +
    • +
    • + + {{ "unlimitedCollections" | i18n }} +
    • +
    +
      +
    • + + {{ "secureDataSharing" | i18n }} +
    • +
    • + + {{ "eventLogMonitoring" | i18n }} +
    • +
    • + + {{ "directoryIntegration" | i18n }} +
    • +
    +

    + {{ "bitwardenSecretsManager" | i18n }} +

    +
      +
    • + + {{ "unlimitedSecretsStorage" | i18n }} +
    • +
    • + + {{ "unlimitedProjects" | i18n }} +
    • +
    • + + {{ "UpTo20MachineAccounts" | i18n }} +
    • +
    +
    +
    +
    +
    +
    + + {{ "secretsManagerSubInfo" | i18n }} + + + {{ "secretsManagerComplimentaryPasswordManager" | i18n }} + +
    +
    + + +

    {{ "paymentMethod" | i18n }}

    +

    + + {{ billing.paymentSource.description }} + {{ + "changePaymentMethod" | i18n + }} + +

    + + + + +
    +

    + {{ "total" | i18n }}: {{ total | currency: "USD" : "$" }} USD + / {{ selectedPlanInterval | i18n }} + +

    +
    + +
    + +

    + {{ "passwordManager" | i18n }} +

    +

    + + {{ passwordManagerSeats }} + {{ "members" | i18n }} × + {{ + (selectedPlan.isAnnual + ? selectedPlan.PasswordManager.basePrice / 12 + : selectedPlan.PasswordManager.basePrice + ) | currency: "$" + }} + /{{ "year" | i18n }} + + + + {{ + selectedPlan.PasswordManager.basePrice | currency: "$" + }} + {{ "freeWithSponsorship" | i18n }} + + + {{ selectedPlan.PasswordManager.basePrice | currency: "$" }} + + +

    +

    + + {{ "additionalUsers" | i18n }}: + {{ passwordManagerSeats || 0 }}  + {{ "members" | i18n }} + × + {{ selectedPlan.PasswordManager.seatPrice | currency: "$" }} + /{{ "year" | i18n }} + + + + {{ passwordManagerSeatTotal(selectedPlan) | currency: "$" }} + +

    +

    + + {{ storageGb }} + {{ "additionalStorageGbMessage" | i18n }} + × + {{ additionalStoragePriceMonthly(selectedPlan) | currency: "$" }} + /{{ "year" | i18n }} + + {{ additionalStorageTotal(selectedPlan) | currency: "$" }} +

    + +

    + + + {{ + "providerDiscount" + | i18n: this.discountPercentageFromSub + this.discountPercentage + | lowercase + }} + + {{ + calculateTotalAppliedDiscount( + passwordManagerSeatTotal(selectedPlan) + additionalStorageTotal(selectedPlan) + ) | currency: "$" + }} + +

    + +

    + {{ "secretsManager" | i18n }} +

    +

    + + {{ sub?.smSeats }} + {{ "members" | i18n }} × + {{ + (selectedPlan.isAnnual + ? selectedPlan.SecretsManager.basePrice / 12 + : selectedPlan.SecretsManager.basePrice + ) | currency: "$" + }} + /{{ "year" | i18n }} + +

    +

    + + {{ "additionalUsers" | i18n }}: + {{ sub?.smSeats || 0 }}  + {{ "members" | i18n }} + × + {{ selectedPlan.SecretsManager.seatPrice | currency: "$" }} + /{{ "year" | i18n }} + + + + {{ secretsManagerSeatTotal(selectedPlan, sub.smSeats) | currency: "$" }} + +

    +

    + + {{ additionalServiceAccount }} + {{ "serviceAccounts" | i18n | lowercase }} + × + {{ selectedPlan?.SecretsManager?.additionalPricePerServiceAccount | currency: "$" }} + /{{ "month" | i18n }} + + {{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }} +

    + +

    + + + {{ + "providerDiscount" + | i18n: this.discountPercentageFromSub + this.discountPercentage + | lowercase + }} + + {{ + calculateTotalAppliedDiscount( + additionalServiceAccountTotal(selectedPlan) + + secretsManagerSeatTotal(selectedPlan, sub.smSeats) + ) | currency: "$" + }} + +

    +
    + +

    + {{ "passwordManager" | i18n }} +

    +

    + + {{ "basePrice" | i18n }}: + {{ selectedPlan.PasswordManager.basePrice | currency: "$" }} + {{ "monthAbbr" | i18n }} + + + {{ selectedPlan.PasswordManager.basePrice | currency: "$" }} + +

    +

    + + {{ "additionalUsers" | i18n }}: + {{ passwordManagerSeats }}  + {{ "members" | i18n }} + × + {{ selectedPlan.PasswordManager.seatPrice | currency: "$" }} + /{{ "month" | i18n }} + + + {{ passwordManagerSeatTotal(selectedPlan) | currency: "$" }} + +

    +

    + + {{ storageGb }} + {{ "additionalStorageGbMessage" | i18n }} + × + {{ additionalStoragePriceMonthly(selectedPlan) | currency: "$" }} + /{{ "month" | i18n }} + + {{ + storageGb * selectedPlan.PasswordManager.additionalStoragePricePerGb | currency: "$" + }} +

    + +

    + + + {{ "providerDiscount" | i18n: this.discountPercentageFromSub | lowercase }} + + {{ calculateTotalAppliedDiscount(total) | currency: "$" }} + +

    + +

    + {{ "secretsManager" | i18n }} +

    +

    + + {{ "basePrice" | i18n }}: + {{ selectedPlan.SecretsManager.basePrice | currency: "$" }} + {{ "monthAbbr" | i18n }} + + + {{ selectedPlan.SecretsManager.basePrice | currency: "$" }} + +

    +

    + + {{ "additionalUsers" | i18n }}: + {{ sub?.smSeats }}  + {{ "members" | i18n }} + × + {{ selectedPlan.SecretsManager.seatPrice | currency: "$" }} + /{{ "month" | i18n }} + + + {{ secretsManagerSeatTotal(selectedPlan, sub?.smSeats) | currency: "$" }} + +

    +

    + + {{ additionalServiceAccount }} + {{ "serviceAccounts" | i18n | lowercase }} + × + {{ selectedPlan.SecretsManager.additionalPricePerServiceAccount | currency: "$" }} + /{{ "month" | i18n }} + + {{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }} +

    + +

    + + + {{ "providerDiscount" | i18n: this.discountPercentageFromSub | lowercase }} + + {{ + additionalServiceAccountTotal(selectedPlan) + + secretsManagerSeatTotal(selectedPlan, sub?.smSeats) | currency: "$" + }} + +

    +
    +
    + +
    + + +

    + {{ "secretsManager" | i18n }} +

    +

    + + {{ sub?.smSeats }} + {{ "members" | i18n }} × + {{ + (selectedPlan.isAnnual + ? selectedPlan.SecretsManager.basePrice / 12 + : selectedPlan.SecretsManager.basePrice + ) | currency: "$" + }} + /{{ "year" | i18n }} + +

    +

    + + {{ "additionalUsers" | i18n }}: + {{ sub?.smSeats || 0 }}  + {{ "members" | i18n }} + × + {{ selectedPlan.SecretsManager.seatPrice | currency: "$" }} + /{{ "year" | i18n }} + + + + {{ secretsManagerSeatTotal(selectedPlan, sub.smSeats) | currency: "$" }} + +

    +

    + + {{ additionalServiceAccount }} + {{ "serviceAccounts" | i18n }} + × + {{ selectedPlan.SecretsManager.additionalPricePerServiceAccount | currency: "$" }} + /{{ "month" | i18n }} + + {{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }} +

    + +

    + + + {{ + "providerDiscount" + | i18n: this.discountPercentageFromSub + this.discountPercentage + | lowercase + }} + + {{ + calculateTotalAppliedDiscount( + additionalServiceAccountTotal(selectedPlan) + + secretsManagerSeatTotal(selectedPlan, sub.smSeats) + ) | currency: "$" + }} + +

    + +

    + {{ "passwordManager" | i18n }} +

    +

    + + {{ sub?.seats }} + {{ "members" | i18n }} × + {{ + (selectedPlan.isAnnual + ? selectedPlan.PasswordManager.basePrice / 12 + : selectedPlan.PasswordManager.basePrice + ) | currency: "$" + }} + /{{ "year" | i18n }} + + + + {{ + selectedPlan.PasswordManager.basePrice | currency: "$" + }} + {{ "freeWithSponsorship" | i18n }} + + + {{ selectedPlan.PasswordManager.basePrice | currency: "$" }} + + +

    +

    + + {{ "additionalUsers" | i18n }}: + {{ sub?.seats || 0 }}  + {{ "members" | i18n }} + × + {{ selectedPlan.PasswordManager.seatPrice | currency: "$" }} + /{{ "year" | i18n }} + + + + {{ "freeForOneYear" | i18n }} + + + + {{ passwordManagerSeatTotal(selectedPlan) | currency: "$" }} + +

    +
    + + +

    + {{ "secretsManager" | i18n }} +

    +

    + + {{ "basePrice" | i18n }}: + {{ selectedPlan.SecretsManager.basePrice | currency: "$" }} + {{ "monthAbbr" | i18n }} + + + {{ selectedPlan.SecretsManager.basePrice | currency: "$" }} + +

    +

    + + {{ "additionalUsers" | i18n }}: + {{ sub?.smSeats }}  + {{ "members" | i18n }} + × + {{ selectedPlan.SecretsManager.seatPrice | currency: "$" }} + /{{ "month" | i18n }} + + + {{ secretsManagerSeatTotal(selectedPlan, sub?.smSeats) | currency: "$" }} + +

    +

    + + {{ additionalServiceAccount }} + {{ "serviceAccounts" | i18n }} + × + {{ selectedPlan.SecretsManager.additionalPricePerServiceAccount | currency: "$" }} + /{{ "month" | i18n }} + + {{ additionalServiceAccountTotal(selectedPlan) | currency: "$" }} +

    + +

    + {{ "passwordManager" | i18n }} +

    +

    + + {{ "basePrice" | i18n }}: + {{ selectedPlan.PasswordManager.basePrice | currency: "$" }} + {{ "monthAbbr" | i18n }} + + + {{ selectedPlan.PasswordManager.basePrice | currency: "$" }} + +

    +

    + + {{ "additionalUsers" | i18n }}: + {{ sub?.seats }}  + {{ "members" | i18n }} + × + {{ selectedPlan.PasswordManager.seatPrice | currency: "$" }} + /{{ "month" | i18n }} + + + {{ "freeForOneYear" | i18n }} + + + + {{ passwordManagerSeatTotal(selectedPlan) | currency: "$" }} + +

    +
    +
    + +
    + +

    + + + {{ + "providerDiscount" + | i18n: this.discountPercentageFromSub + this.discountPercentage + | lowercase + }} + + {{ + calculateTotalAppliedDiscount(total) | currency: "$" + }} + + + + {{ "providerDiscount" | i18n: this.discountPercentageFromSub | lowercase }} + + {{ calculateTotalAppliedDiscount(total) | currency: "$" }} + +

    +
    +
    +
    + +

    + + {{ "total" | i18n }} + + + {{ total | currency: "USD" : "$" }} + + / {{ selectedPlanInterval | i18n }} + +

    +
    +
    +
    +
    + + + + +
    + diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts new file mode 100644 index 00000000000..dc9f6cce688 --- /dev/null +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts @@ -0,0 +1,864 @@ +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { + Component, + EventEmitter, + Inject, + Input, + OnDestroy, + OnInit, + Output, + ViewChild, +} from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; +import { Router } from "@angular/router"; +import { Subject, takeUntil } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; +import { OrganizationUpgradeRequest } from "@bitwarden/common/admin-console/models/request/organization-upgrade.request"; +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; +import { + PaymentMethodType, + PlanInterval, + PlanType, + ProductTierType, +} from "@bitwarden/common/billing/enums"; +import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment.request"; +import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request"; +import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response"; +import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; +import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { DialogService, ToastService } from "@bitwarden/components"; + +import { PaymentV2Component } from "../shared/payment/payment-v2.component"; +import { PaymentComponent } from "../shared/payment/payment.component"; +import { TaxInfoComponent } from "../shared/tax-info.component"; + +type ChangePlanDialogParams = { + organizationId: string; + subscription: OrganizationSubscriptionResponse; + productTierType: ProductTierType; +}; + +export enum ChangePlanDialogResultType { + Closed = "closed", + Submitted = "submitted", +} + +export enum PlanCardState { + Selected = "selected", + NotSelected = "not_selected", + Disabled = "disabled", +} + +export const openChangePlanDialog = ( + dialogService: DialogService, + dialogConfig: DialogConfig, +) => + dialogService.open( + ChangePlanDialogComponent, + dialogConfig, + ); + +type PlanCard = { + name: string; + selected: boolean; +}; + +interface OnSuccessArgs { + organizationId: string; +} + +@Component({ + templateUrl: "./change-plan-dialog.component.html", +}) +export class ChangePlanDialogComponent implements OnInit, OnDestroy { + @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; + @ViewChild(PaymentV2Component) paymentV2Component: PaymentV2Component; + @ViewChild(TaxInfoComponent) taxComponent: TaxInfoComponent; + + @Input() acceptingSponsorship = false; + @Input() organizationId: string; + @Input() showFree = false; + @Input() showCancel = false; + selectedFile: File; + + @Input() + get productTier(): ProductTierType { + return this._productTier; + } + + set productTier(product: ProductTierType) { + this._productTier = product; + this.formGroup?.controls?.productTier?.setValue(product); + } + + private _productTier = ProductTierType.Free; + + @Input() + get plan(): PlanType { + return this._plan; + } + + set plan(plan: PlanType) { + this._plan = plan; + this.formGroup?.controls?.plan?.setValue(plan); + } + + private _plan = PlanType.Free; + @Input() providerId?: string; + @Output() onSuccess = new EventEmitter(); + @Output() onCanceled = new EventEmitter(); + @Output() onTrialBillingSuccess = new EventEmitter(); + + protected discountPercentage: number = 20; + protected discountPercentageFromSub: number; + protected loading = true; + protected planCards: PlanCard[]; + protected ResultType = ChangePlanDialogResultType; + + selfHosted = false; + productTypes = ProductTierType; + formPromise: Promise; + singleOrgPolicyAppliesToActiveUser = false; + isInTrialFlow = false; + discount = 0; + + formGroup = this.formBuilder.group({ + name: [""], + billingEmail: ["", [Validators.email]], + businessOwned: [false], + premiumAccessAddon: [false], + additionalSeats: [0, [Validators.min(0), Validators.max(100000)]], + clientOwnerEmail: ["", [Validators.email]], + plan: [this.plan], + productTier: [this.productTier], + // planInterval: [1], + }); + + planType: string; + selectedPlan: PlanResponse; + selectedInterval: number = 1; + planIntervals = PlanInterval; + passwordManagerPlans: PlanResponse[]; + secretsManagerPlans: PlanResponse[]; + organization: Organization; + sub: OrganizationSubscriptionResponse; + billing: BillingResponse; + currentPlanName: string; + showPayment: boolean = false; + totalOpened: boolean = false; + currentPlan: PlanResponse; + + deprecateStripeSourcesAPI: boolean; + + private destroy$ = new Subject(); + + constructor( + @Inject(DIALOG_DATA) private dialogParams: ChangePlanDialogParams, + private dialogRef: DialogRef, + private toastService: ToastService, + private apiService: ApiService, + private i18nService: I18nService, + private cryptoService: CryptoService, + private router: Router, + private syncService: SyncService, + private policyService: PolicyService, + private organizationService: OrganizationService, + private messagingService: MessagingService, + private formBuilder: FormBuilder, + private organizationApiService: OrganizationApiServiceAbstraction, + private configService: ConfigService, + private billingApiService: BillingApiServiceAbstraction, + ) {} + + async ngOnInit(): Promise { + this.deprecateStripeSourcesAPI = await this.configService.getFeatureFlag( + FeatureFlag.AC2476_DeprecateStripeSourcesAPI, + ); + + if (this.dialogParams.organizationId) { + this.currentPlanName = this.resolvePlanName(this.dialogParams.productTierType); + this.sub = + this.dialogParams.subscription ?? + (await this.organizationApiService.getSubscription(this.dialogParams.organizationId)); + this.organizationId = this.dialogParams.organizationId; + this.currentPlan = this.sub?.plan; + this.selectedPlan = this.sub?.plan; + this.organization = await this.organizationService.get(this.organizationId); + this.billing = await this.organizationApiService.getBilling(this.organizationId); + } + + if (!this.selfHosted) { + const plans = await this.apiService.getPlans(); + this.passwordManagerPlans = plans.data.filter((plan) => !!plan.PasswordManager); + this.secretsManagerPlans = plans.data.filter((plan) => !!plan.SecretsManager); + + if ( + this.productTier === ProductTierType.Enterprise || + this.productTier === ProductTierType.Teams + ) { + this.formGroup.controls.businessOwned.setValue(true); + } + } + + if (this.currentPlan && this.currentPlan.productTier !== ProductTierType.Enterprise) { + const upgradedPlan = this.passwordManagerPlans.find((plan) => + this.currentPlan.productTier === ProductTierType.Free + ? plan.type === PlanType.FamiliesAnnually + : plan.upgradeSortOrder == this.currentPlan.upgradeSortOrder + 1, + ); + + this.plan = upgradedPlan.type; + this.productTier = upgradedPlan.productTier; + } + this.upgradeFlowPrefillForm(); + + this.policyService + .policyAppliesToActiveUser$(PolicyType.SingleOrg) + .pipe(takeUntil(this.destroy$)) + .subscribe((policyAppliesToActiveUser) => { + this.singleOrgPolicyAppliesToActiveUser = policyAppliesToActiveUser; + }); + + if (!this.selfHosted) { + this.changedProduct(); + } + + this.planCards = [ + { + name: this.i18nService.t("planNameTeams"), + selected: true, + }, + { + name: this.i18nService.t("planNameEnterprise"), + selected: false, + }, + ]; + this.discountPercentageFromSub = this.isSecretsManagerTrial() + ? 0 + : (this.sub?.customerDiscount?.percentOff ?? 0); + + this.setInitialPlanSelection(); + this.loading = false; + } + + setInitialPlanSelection() { + this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); + } + + getPlanByType(productTier: ProductTierType) { + return this.selectableProducts.find((product) => product.productTier === productTier); + } + + secretsManagerTrialDiscount() { + return this.sub?.customerDiscount?.appliesTo?.includes("sm-standalone") + ? this.discountPercentage + : this.discountPercentageFromSub + this.discountPercentage; + } + + isSecretsManagerTrial(): boolean { + return ( + this.sub?.subscription?.items?.some((item) => + this.sub?.customerDiscount?.appliesTo?.includes(item.productId), + ) ?? false + ); + } + + planTypeChanged() { + this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); + } + + updateInterval(event: number) { + this.selectedInterval = event; + this.planTypeChanged(); + } + + protected getPlanIntervals() { + return [ + { + name: PlanInterval[PlanInterval.Annually], + value: PlanInterval.Annually, + }, + { + name: PlanInterval[PlanInterval.Monthly], + value: PlanInterval.Monthly, + }, + ]; + } + + optimizedNgForRender(index: number) { + return index; + } + + protected getPlanCardContainerClasses(plan: PlanResponse, index: number) { + let cardState: PlanCardState; + + if (plan == this.currentPlan) { + cardState = PlanCardState.Disabled; + } else if (plan == this.selectedPlan) { + cardState = PlanCardState.Selected; + } else if ( + this.selectedInterval === PlanInterval.Monthly && + plan.productTier == ProductTierType.Families + ) { + cardState = PlanCardState.Disabled; + } else { + cardState = PlanCardState.NotSelected; + } + + switch (cardState) { + case PlanCardState.Selected: { + return [ + "tw-group", + "tw-cursor-pointer", + "tw-block", + "tw-rounded", + "tw-border", + "tw-border-solid", + "tw-border-primary-600", + "hover:tw-border-primary-700", + "focus:tw-border-2", + "focus:tw-border-primary-700", + "focus:tw-rounded-lg", + ]; + } + case PlanCardState.NotSelected: { + return [ + "tw-cursor-pointer", + "tw-block", + "tw-rounded", + "tw-border", + "tw-border-solid", + "tw-border-secondary-300", + "hover:tw-border-text-main", + "focus:tw-border-2", + "focus:tw-border-primary-700", + ]; + } + case PlanCardState.Disabled: { + return [ + "tw-cursor-not-allowed", + "tw-bg-secondary-100", + "tw-font-normal", + "tw-bg-blur", + "tw-text-muted", + "tw-block", + "tw-rounded", + ]; + } + } + } + + protected selectPlan(plan: PlanResponse) { + if ( + this.selectedInterval === PlanInterval.Monthly && + plan.productTier == ProductTierType.Families + ) { + return; + } + + if (plan === this.currentPlan) { + return; + } + this.selectedPlan = plan; + this.formGroup.patchValue({ productTier: plan.productTier }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + get upgradeRequiresPaymentMethod() { + return ( + this.organization?.productTierType === ProductTierType.Free && + !this.showFree && + !this.billing?.paymentSource + ); + } + + get selectedSecretsManagerPlan() { + return this.secretsManagerPlans.find((plan) => plan.type === this.selectedPlan.type); + } + + get selectedPlanInterval() { + return this.selectedPlan.isAnnual ? "year" : "month"; + } + + get selectableProducts() { + if (this.acceptingSponsorship) { + const familyPlan = this.passwordManagerPlans.find( + (plan) => plan.type === PlanType.FamiliesAnnually, + ); + this.discount = familyPlan.PasswordManager.basePrice; + return [familyPlan]; + } + + const businessOwnedIsChecked = this.formGroup.controls.businessOwned.value; + + const result = this.passwordManagerPlans.filter( + (plan) => + plan.type !== PlanType.Custom && + (!businessOwnedIsChecked || plan.canBeUsedByBusiness) && + (this.showFree || plan.productTier !== ProductTierType.Free) && + (plan.productTier === ProductTierType.Free || + plan.productTier === ProductTierType.TeamsStarter || + (this.selectedInterval === PlanInterval.Annually && plan.isAnnual) || + (this.selectedInterval === PlanInterval.Monthly && !plan.isAnnual)) && + (!this.currentPlan || this.currentPlan.upgradeSortOrder < plan.upgradeSortOrder) && + this.planIsEnabled(plan), + ); + + if ( + this.currentPlan.productTier === ProductTierType.Free && + this.selectedInterval === PlanInterval.Monthly && + !this.organization.useSecretsManager + ) { + const familyPlan = this.passwordManagerPlans.find( + (plan) => plan.productTier == ProductTierType.Families, + ); + result.push(familyPlan); + } + + if ( + this.organization.useSecretsManager && + this.currentPlan.productTier === ProductTierType.Free + ) { + const familyPlanIndex = result.findIndex( + (plan) => plan.productTier === ProductTierType.Families, + ); + + if (familyPlanIndex !== -1) { + result.splice(familyPlanIndex, 1); + } + } + + if (this.currentPlan.productTier !== ProductTierType.Free) { + result.push(this.currentPlan); + } + + result.sort((planA, planB) => planA.displaySortOrder - planB.displaySortOrder); + + return result; + } + + get selectablePlans() { + const selectedProductTierType = this.formGroup.controls.productTier.value; + const result = + this.passwordManagerPlans?.filter( + (plan) => plan.productTier === selectedProductTierType && this.planIsEnabled(plan), + ) || []; + + result.sort((planA, planB) => planA.displaySortOrder - planB.displaySortOrder); + return result; + } + + get storageGb() { + return this.sub?.maxStorageGb - 1; + } + + passwordManagerSeatTotal(plan: PlanResponse): number { + if (!plan.PasswordManager.hasAdditionalSeatsOption || this.isSecretsManagerTrial()) { + return 0; + } + + const result = plan.PasswordManager.seatPrice * Math.abs(this.organization.seats || 0); + return result; + } + + secretsManagerSeatTotal(plan: PlanResponse, seats: number): number { + if (!plan.SecretsManager.hasAdditionalSeatsOption) { + return 0; + } + + return plan.SecretsManager.seatPrice * Math.abs(seats || 0); + } + + additionalStorageTotal(plan: PlanResponse): number { + if (!plan.PasswordManager.hasAdditionalStorageOption) { + return 0; + } + + return ( + plan.PasswordManager.additionalStoragePricePerGb * Math.abs(this.sub?.maxStorageGb - 1 || 0) + ); + } + + additionalStoragePriceMonthly(selectedPlan: PlanResponse) { + if (!selectedPlan.isAnnual) { + return selectedPlan.PasswordManager.additionalStoragePricePerGb; + } + return selectedPlan.PasswordManager.additionalStoragePricePerGb / 12; + } + + additionalServiceAccountTotal(plan: PlanResponse): number { + if ( + !plan.SecretsManager.hasAdditionalServiceAccountOption || + this.additionalServiceAccount == 0 + ) { + return 0; + } + + return plan.SecretsManager.additionalPricePerServiceAccount * this.additionalServiceAccount; + } + + get passwordManagerSubtotal() { + let subTotal = this.selectedPlan.PasswordManager.basePrice; + if (this.selectedPlan.PasswordManager.hasAdditionalSeatsOption) { + subTotal += this.passwordManagerSeatTotal(this.selectedPlan); + } + if (this.selectedPlan.PasswordManager.hasPremiumAccessOption) { + subTotal += this.selectedPlan.PasswordManager.premiumAccessOptionPrice; + } + return subTotal - this.discount; + } + + get secretsManagerSubtotal() { + const plan = this.selectedSecretsManagerPlan; + + if (!this.organization.useSecretsManager) { + return 0; + } + + return ( + plan.SecretsManager.basePrice + + this.secretsManagerSeatTotal(plan, this.sub?.smSeats) + + this.additionalServiceAccountTotal(plan) + ); + } + + get taxCharges() { + return this.taxComponent != null && this.taxComponent.taxRate != null + ? (this.taxComponent.taxRate / 100) * this.passwordManagerSubtotal + : 0; + } + + get passwordManagerSeats() { + if (this.selectedPlan.productTier === ProductTierType.Families) { + return this.selectedPlan.PasswordManager.baseSeats; + } + return this.sub?.seats; + } + + get total() { + if (this.organization.useSecretsManager) { + return ( + this.passwordManagerSubtotal + + this.additionalStorageTotal(this.selectedPlan) + + this.secretsManagerSubtotal + + this.taxCharges || 0 + ); + } + return ( + this.passwordManagerSubtotal + + this.additionalStorageTotal(this.selectedPlan) + + this.taxCharges || 0 + ); + } + + get teamsStarterPlanIsAvailable() { + return this.selectablePlans.some((plan) => plan.type === PlanType.TeamsStarter); + } + + get additionalServiceAccount() { + const baseServiceAccount = this.currentPlan.SecretsManager?.baseServiceAccount || 0; + const usedServiceAccounts = this.sub?.smServiceAccounts || 0; + + const additionalServiceAccounts = baseServiceAccount - usedServiceAccounts; + + return additionalServiceAccounts <= 0 ? Math.abs(additionalServiceAccounts) : 0; + } + + changedProduct() { + const selectedPlan = this.selectablePlans[0]; + + this.setPlanType(selectedPlan.type); + this.handlePremiumAddonAccess(selectedPlan.PasswordManager.hasPremiumAccessOption); + this.handleAdditionalSeats(selectedPlan.PasswordManager.hasAdditionalSeatsOption); + } + + setPlanType(planType: PlanType) { + this.formGroup.controls.plan.setValue(planType); + } + + handlePremiumAddonAccess(hasPremiumAccessOption: boolean) { + this.formGroup.controls.premiumAccessAddon.setValue(!hasPremiumAccessOption); + } + + handleAdditionalSeats(selectedPlanHasAdditionalSeatsOption: boolean) { + if (!selectedPlanHasAdditionalSeatsOption) { + this.formGroup.controls.additionalSeats.setValue(0); + return; + } + + if (this.currentPlan && !this.currentPlan.PasswordManager.hasAdditionalSeatsOption) { + this.formGroup.controls.additionalSeats.setValue(this.currentPlan.PasswordManager.baseSeats); + return; + } + + if (this.organization) { + this.formGroup.controls.additionalSeats.setValue(this.organization.seats); + return; + } + + this.formGroup.controls.additionalSeats.setValue(1); + } + + changedCountry() { + if (this.deprecateStripeSourcesAPI && this.paymentV2Component && this.taxComponent) { + this.paymentV2Component.showBankAccount = this.taxComponent.country === "US"; + + if ( + !this.paymentV2Component.showBankAccount && + this.paymentV2Component.selected === PaymentMethodType.BankAccount + ) { + this.paymentV2Component.select(PaymentMethodType.Card); + } + } else if (this.paymentComponent && this.taxComponent) { + this.paymentComponent!.hideBank = this.taxComponent?.taxFormGroup?.value.country !== "US"; + // Bank Account payments are only available for US customers + if ( + this.paymentComponent.hideBank && + this.paymentComponent.method === PaymentMethodType.BankAccount + ) { + this.paymentComponent.method = PaymentMethodType.Card; + this.paymentComponent.changeMethod(); + } + } + } + + submit = async () => { + if (!this.taxComponent?.taxFormGroup.valid && this.taxComponent?.taxFormGroup.touched) { + this.taxComponent?.taxFormGroup.markAllAsTouched(); + return; + } + + const doSubmit = async (): Promise => { + let orgId: string = null; + orgId = await this.updateOrganization(); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("organizationUpgraded"), + }); + + await this.apiService.refreshIdentityToken(); + await this.syncService.fullSync(true); + + if (!this.acceptingSponsorship && !this.isInTrialFlow) { + // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.router.navigate(["/organizations/" + orgId + "/members"]); + } + + if (this.isInTrialFlow) { + this.onTrialBillingSuccess.emit({ + orgId: orgId, + subLabelText: this.billingSubLabelText(), + }); + } + + return orgId; + }; + + this.formPromise = doSubmit(); + const organizationId = await this.formPromise; + this.onSuccess.emit({ organizationId: organizationId }); + // TODO: No one actually listening to this message? + this.messagingService.send("organizationCreated", { organizationId }); + this.dialogRef.close(); + }; + + private async updateOrganization() { + const request = new OrganizationUpgradeRequest(); + if (this.selectedPlan.productTier !== ProductTierType.Families) { + request.additionalSeats = this.sub?.seats; + } + if (this.sub?.maxStorageGb > this.selectedPlan.PasswordManager.baseStorageGb) { + request.additionalStorageGb = + this.sub?.maxStorageGb - this.selectedPlan.PasswordManager.baseStorageGb; + } + request.premiumAccessAddon = + this.selectedPlan.PasswordManager.hasPremiumAccessOption && + this.formGroup.controls.premiumAccessAddon.value; + request.planType = this.selectedPlan.type; + if (this.showPayment) { + request.billingAddressCountry = this.taxComponent.taxFormGroup?.value.country; + request.billingAddressPostalCode = this.taxComponent.taxFormGroup?.value.postalCode; + } + + // Secrets Manager + this.buildSecretsManagerRequest(request); + + if (this.upgradeRequiresPaymentMethod || this.showPayment) { + if (this.deprecateStripeSourcesAPI) { + const tokenizedPaymentSource = await this.paymentV2Component.tokenize(); + const updatePaymentMethodRequest = new UpdatePaymentMethodRequest(); + updatePaymentMethodRequest.paymentSource = tokenizedPaymentSource; + updatePaymentMethodRequest.taxInformation = { + country: this.taxComponent.country, + postalCode: this.taxComponent.postalCode, + taxId: this.taxComponent.taxId, + line1: this.taxComponent.line1, + line2: this.taxComponent.line2, + city: this.taxComponent.city, + state: this.taxComponent.state, + }; + + await this.billingApiService.updateOrganizationPaymentMethod( + this.organizationId, + updatePaymentMethodRequest, + ); + } else { + const tokenResult = await this.paymentComponent.createPaymentToken(); + const paymentRequest = new PaymentRequest(); + paymentRequest.paymentToken = tokenResult[0]; + paymentRequest.paymentMethodType = tokenResult[1]; + paymentRequest.country = this.taxComponent.taxFormGroup?.value.country; + paymentRequest.postalCode = this.taxComponent.taxFormGroup?.value.postalCode; + await this.organizationApiService.updatePayment(this.organizationId, paymentRequest); + } + } + + // Backfill pub/priv key if necessary + if (!this.organization.hasPublicAndPrivateKeys) { + const orgShareKey = await this.cryptoService.getOrgKey(this.organizationId); + const orgKeys = await this.cryptoService.makeKeyPair(orgShareKey); + request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); + } + + const result = await this.organizationApiService.upgrade(this.organizationId, request); + if (!result.success && result.paymentIntentClientSecret != null) { + await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null); + } + return this.organizationId; + } + + private billingSubLabelText(): string { + const selectedPlan = this.selectedPlan; + const price = + selectedPlan.PasswordManager.basePrice === 0 + ? selectedPlan.PasswordManager.seatPrice + : selectedPlan.PasswordManager.basePrice; + let text = ""; + + if (selectedPlan.isAnnual) { + text += `${this.i18nService.t("annual")} ($${price}/${this.i18nService.t("yr")})`; + } else { + text += `${this.i18nService.t("monthly")} ($${price}/${this.i18nService.t("monthAbbr")})`; + } + + return text; + } + + private buildSecretsManagerRequest(request: OrganizationUpgradeRequest): void { + request.useSecretsManager = this.organization.useSecretsManager; + if (!this.organization.useSecretsManager) { + return; + } + + if ( + this.selectedPlan.SecretsManager.hasAdditionalSeatsOption && + this.currentPlan.productTier === ProductTierType.Free + ) { + request.additionalSmSeats = this.organization.seats; + } else { + request.additionalSmSeats = this.sub?.smSeats; + request.additionalServiceAccounts = this.additionalServiceAccount; + } + } + + private upgradeFlowPrefillForm() { + if (this.acceptingSponsorship) { + this.formGroup.controls.productTier.setValue(ProductTierType.Families); + this.changedProduct(); + return; + } + + if (this.currentPlan && this.currentPlan.productTier !== ProductTierType.Enterprise) { + const upgradedPlan = this.passwordManagerPlans.find((plan) => { + if (this.currentPlan.productTier === ProductTierType.Free) { + return plan.type === PlanType.FamiliesAnnually; + } + + if ( + this.currentPlan.productTier === ProductTierType.Families && + !this.teamsStarterPlanIsAvailable + ) { + return plan.type === PlanType.TeamsAnnually; + } + + return plan.upgradeSortOrder === this.currentPlan.upgradeSortOrder + 1; + }); + + this.plan = upgradedPlan.type; + this.productTier = upgradedPlan.productTier; + this.changedProduct(); + } + } + + private planIsEnabled(plan: PlanResponse) { + return !plan.disabled && !plan.legacyYear; + } + + toggleShowPayment() { + this.showPayment = true; + } + + toggleTotalOpened() { + this.totalOpened = !this.totalOpened; + } + + calculateTotalAppliedDiscount(total: number) { + const discountPercent = + this.selectedInterval == PlanInterval.Annually + ? this.discountPercentage + this.discountPercentageFromSub + : this.discountPercentageFromSub; + + const discountedTotal = total / (1 - discountPercent / 100); + return discountedTotal; + } + + get paymentSourceClasses() { + if (this.billing.paymentSource == null) { + return []; + } + switch (this.billing.paymentSource.type) { + case PaymentMethodType.Card: + return ["bwi-credit-card"]; + case PaymentMethodType.BankAccount: + return ["bwi-bank"]; + case PaymentMethodType.Check: + return ["bwi-money"]; + case PaymentMethodType.PayPal: + return ["bwi-paypal text-primary"]; + default: + return []; + } + } + + resolvePlanName(productTier: ProductTierType) { + switch (productTier) { + case ProductTierType.Enterprise: + return this.i18nService.t("planNameEnterprise"); + case ProductTierType.Free: + return this.i18nService.t("planNameFree"); + case ProductTierType.Families: + return this.i18nService.t("planNameFamilies"); + case ProductTierType.Teams: + return this.i18nService.t("planNameTeams"); + } + } +} diff --git a/apps/web/src/app/billing/organizations/change-plan.component.html b/apps/web/src/app/billing/organizations/change-plan.component.html index a25dde4fd30..75a12122d19 100644 --- a/apps/web/src/app/billing/organizations/change-plan.component.html +++ b/apps/web/src/app/billing/organizations/change-plan.component.html @@ -18,6 +18,7 @@ [showCancel]="true" [organizationId]="organizationId" [currentPlan]="currentPlan" + [preSelectedProductTier]="preSelectedProductTier" (onCanceled)="cancel()" > diff --git a/apps/web/src/app/billing/organizations/change-plan.component.ts b/apps/web/src/app/billing/organizations/change-plan.component.ts index a131e344b7a..51cdbba557e 100644 --- a/apps/web/src/app/billing/organizations/change-plan.component.ts +++ b/apps/web/src/app/billing/organizations/change-plan.component.ts @@ -1,5 +1,6 @@ import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { ProductTierType } from "@bitwarden/common/billing/enums"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -10,6 +11,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" export class ChangePlanComponent { @Input() organizationId: string; @Input() currentPlan: PlanResponse; + @Input() preSelectedProductTier: ProductTierType; @Output() onChanged = new EventEmitter(); @Output() onCanceled = new EventEmitter(); diff --git a/apps/web/src/app/billing/organizations/download-license.component.html b/apps/web/src/app/billing/organizations/download-license.component.html index 33a534bacf7..b54cdda1f78 100644 --- a/apps/web/src/app/billing/organizations/download-license.component.html +++ b/apps/web/src/app/billing/organizations/download-license.component.html @@ -14,6 +14,7 @@ rel="noreferrer" appA11yTitle="{{ 'learnMore' | i18n }}" href="https://bitwarden.com/help/licensing-on-premise/#organization-account-sharing" + slot="end" > diff --git a/apps/web/src/app/billing/organizations/organization-billing-history-view.component.html b/apps/web/src/app/billing/organizations/organization-billing-history-view.component.html index 087009b2910..20bf0475eed 100644 --- a/apps/web/src/app/billing/organizations/organization-billing-history-view.component.html +++ b/apps/web/src/app/billing/organizations/organization-billing-history-view.component.html @@ -1,17 +1,4 @@ - - - + @@ -22,7 +9,16 @@ > {{ "loading" | i18n }} - - + + + diff --git a/apps/web/src/app/billing/organizations/organization-billing-history-view.component.ts b/apps/web/src/app/billing/organizations/organization-billing-history-view.component.ts index cd293452001..00ab3fa7772 100644 --- a/apps/web/src/app/billing/organizations/organization-billing-history-view.component.ts +++ b/apps/web/src/app/billing/organizations/organization-billing-history-view.component.ts @@ -2,8 +2,11 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { concatMap, Subject, takeUntil } from "rxjs"; -import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; -import { BillingHistoryResponse } from "@bitwarden/common/billing/models/response/billing-history.response"; +import { OrganizationBillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/organizations/organization-billing-api.service.abstraction"; +import { + BillingInvoiceResponse, + BillingTransactionResponse, +} from "@bitwarden/common/billing/models/response/billing.response"; @Component({ templateUrl: "organization-billing-history-view.component.html", @@ -11,13 +14,15 @@ import { BillingHistoryResponse } from "@bitwarden/common/billing/models/respons export class OrgBillingHistoryViewComponent implements OnInit, OnDestroy { loading = false; firstLoaded = false; - billing: BillingHistoryResponse; + invoices: BillingInvoiceResponse[] = []; + transactions: BillingTransactionResponse[] = []; organizationId: string; + hasAdditionalHistory: boolean = false; private destroy$ = new Subject(); constructor( - private organizationApiService: OrganizationApiServiceAbstraction, + private organizationBillingApiService: OrganizationBillingApiServiceAbstraction, private route: ActivatedRoute, ) {} @@ -43,8 +48,28 @@ export class OrgBillingHistoryViewComponent implements OnInit, OnDestroy { if (this.loading) { return; } + this.loading = true; - this.billing = await this.organizationApiService.getBillingHistory(this.organizationId); + + const invoicesPromise = this.organizationBillingApiService.getBillingInvoices( + this.organizationId, + this.invoices.length > 0 ? this.invoices[this.invoices.length - 1].id : null, + ); + + const transactionsPromise = this.organizationBillingApiService.getBillingTransactions( + this.organizationId, + this.transactions.length > 0 + ? this.transactions[this.transactions.length - 1].createdDate + : null, + ); + + const invoices = await invoicesPromise; + const transactions = await transactionsPromise; + const pageSize = 5; + + this.invoices = [...this.invoices, ...invoices]; + this.transactions = [...this.transactions, ...transactions]; + this.hasAdditionalHistory = !(invoices.length < pageSize && transactions.length < pageSize); this.loading = false; } } diff --git a/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts b/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts index 970b40a54bd..3d4c8dd3870 100644 --- a/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing-routing.module.ts @@ -1,7 +1,9 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; +import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route"; import { canAccessBillingTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { organizationPermissionsGuard } from "../../admin-console/organizations/guards/org-permissions.guard"; import { organizationIsUnmanaged } from "../../billing/guards/organization-is-unmanaged.guard"; @@ -11,6 +13,7 @@ import { PaymentMethodComponent } from "../shared"; import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component"; import { OrganizationSubscriptionCloudComponent } from "./organization-subscription-cloud.component"; import { OrganizationSubscriptionSelfhostComponent } from "./organization-subscription-selfhost.component"; +import { OrganizationPaymentMethodComponent } from "./payment-method/organization-payment-method.component"; const routes: Routes = [ { @@ -25,17 +28,21 @@ const routes: Routes = [ : OrganizationSubscriptionCloudComponent, data: { titleId: "subscription" }, }, - { - path: "payment-method", - component: PaymentMethodComponent, - canActivate: [ - organizationPermissionsGuard((org) => org.canEditPaymentMethods), - organizationIsUnmanaged, - ], - data: { - titleId: "paymentMethod", + ...featureFlaggedRoute({ + defaultComponent: PaymentMethodComponent, + flaggedComponent: OrganizationPaymentMethodComponent, + featureFlag: FeatureFlag.AC2476_DeprecateStripeSourcesAPI, + routeOptions: { + path: "payment-method", + canActivate: [ + organizationPermissionsGuard((org) => org.canEditPaymentMethods), + organizationIsUnmanaged, + ], + data: { + titleId: "paymentMethod", + }, }, - }, + }), { path: "history", component: OrgBillingHistoryViewComponent, diff --git a/apps/web/src/app/billing/organizations/organization-billing.module.ts b/apps/web/src/app/billing/organizations/organization-billing.module.ts index a95efe32e47..ccfe12b2e59 100644 --- a/apps/web/src/app/billing/organizations/organization-billing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing.module.ts @@ -7,6 +7,7 @@ import { BillingSharedModule } from "../shared"; import { AdjustSubscription } from "./adjust-subscription.component"; import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component"; import { BillingSyncKeyComponent } from "./billing-sync-key.component"; +import { ChangePlanDialogComponent } from "./change-plan-dialog.component"; import { ChangePlanComponent } from "./change-plan.component"; import { DownloadLicenceDialogComponent } from "./download-license.component"; import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component"; @@ -14,6 +15,7 @@ import { OrganizationBillingRoutingModule } from "./organization-billing-routing import { OrganizationPlansComponent } from "./organization-plans.component"; import { OrganizationSubscriptionCloudComponent } from "./organization-subscription-cloud.component"; import { OrganizationSubscriptionSelfhostComponent } from "./organization-subscription-selfhost.component"; +import { OrganizationPaymentMethodComponent } from "./payment-method/organization-payment-method.component"; import { SecretsManagerAdjustSubscriptionComponent } from "./sm-adjust-subscription.component"; import { SecretsManagerSubscribeStandaloneComponent } from "./sm-subscribe-standalone.component"; import { SubscriptionHiddenComponent } from "./subscription-hidden.component"; @@ -40,6 +42,8 @@ import { SubscriptionStatusComponent } from "./subscription-status.component"; SecretsManagerSubscribeStandaloneComponent, SubscriptionHiddenComponent, SubscriptionStatusComponent, + ChangePlanDialogComponent, + OrganizationPaymentMethodComponent, ], }) export class OrganizationBillingModule {} diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.html b/apps/web/src/app/billing/organizations/organization-plans.component.html index b9a3cc6bf05..498374aa14b 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.html +++ b/apps/web/src/app/billing/organizations/organization-plans.component.html @@ -19,12 +19,13 @@ {{ "licenseFileDesc" | i18n: "bitwarden_organization_license.json" }} @@ -427,9 +428,12 @@ {{ paymentDesc }}

    +
    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 661b8969136..3d02fa027ef 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -28,6 +28,8 @@ import { PaymentRequest } from "@bitwarden/common/billing/models/request/payment import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -37,10 +39,12 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { OrgKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; import { OrganizationCreateModule } from "../../admin-console/organizations/create/organization-create.module"; import { BillingSharedModule, secretsManagerSubscribeFormFactory } from "../shared"; -import { PaymentComponent } from "../shared/payment.component"; +import { PaymentV2Component } from "../shared/payment/payment-v2.component"; +import { PaymentComponent } from "../shared/payment/payment.component"; import { TaxInfoComponent } from "../shared/tax-info.component"; interface OnSuccessArgs { @@ -62,6 +66,7 @@ const Allowed2020PlansForLegacyProviders = [ }) export class OrganizationPlansComponent implements OnInit, OnDestroy { @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; + @ViewChild(PaymentV2Component) paymentV2Component: PaymentV2Component; @ViewChild(TaxInfoComponent) taxComponent: TaxInfoComponent; @Input() organizationId: string; @@ -95,6 +100,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { private _plan = PlanType.Free; @Input() providerId?: string; + @Input() preSelectedProductTier?: ProductTierType; @Output() onSuccess = new EventEmitter(); @Output() onCanceled = new EventEmitter(); @Output() onTrialBillingSuccess = new EventEmitter(); @@ -106,6 +112,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { singleOrgPolicyAppliesToActiveUser = false; isInTrialFlow = false; discount = 0; + deprecateStripeSourcesAPI: boolean; secretsManagerSubscription = secretsManagerSubscribeFormFactory(this.formBuilder); @@ -149,11 +156,17 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { private formBuilder: FormBuilder, private organizationApiService: OrganizationApiServiceAbstraction, private providerApiService: ProviderApiServiceAbstraction, + private toastService: ToastService, + private configService: ConfigService, ) { this.selfHosted = platformUtilsService.isSelfHost(); } async ngOnInit() { + this.deprecateStripeSourcesAPI = await this.configService.getFeatureFlag( + FeatureFlag.AC2476_DeprecateStripeSourcesAPI, + ); + if (this.organizationId) { this.organization = await this.organizationService.get(this.organizationId); this.billing = await this.organizationApiService.getBilling(this.organizationId); @@ -186,6 +199,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { if (this.hasProvider) { this.formGroup.controls.businessOwned.setValue(true); + this.formGroup.controls.clientOwnerEmail.addValidators(Validators.required); this.changedOwnedBusiness(); this.provider = await this.providerApiService.getProvider(this.providerId); const providerDefaultPlan = this.passwordManagerPlans.find( @@ -209,6 +223,9 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.singleOrgPolicyAppliesToActiveUser = policyAppliesToActiveUser; }); + if (this.preSelectedProductTier != null && this.productTier < this.preSelectedProductTier) { + this.productTier = this.preSelectedProductTier; + } if (!this.selfHosted) { this.changedProduct(); } @@ -528,14 +545,23 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { } changedCountry() { - this.paymentComponent.hideBank = this.taxComponent.taxFormGroup?.value.country !== "US"; - // Bank Account payments are only available for US customers - if ( - this.paymentComponent.hideBank && - this.paymentComponent.method === PaymentMethodType.BankAccount - ) { - this.paymentComponent.method = PaymentMethodType.Card; - this.paymentComponent.changeMethod(); + if (this.deprecateStripeSourcesAPI) { + this.paymentV2Component.showBankAccount = this.taxComponent.country === "US"; + if ( + !this.paymentV2Component.showBankAccount && + this.paymentV2Component.selected === PaymentMethodType.BankAccount + ) { + this.paymentV2Component.select(PaymentMethodType.Card); + } + } else { + this.paymentComponent.hideBank = this.taxComponent.taxFormGroup?.value.country !== "US"; + if ( + this.paymentComponent.hideBank && + this.paymentComponent.method === PaymentMethodType.BankAccount + ) { + this.paymentComponent.method = PaymentMethodType.Card; + this.paymentComponent.changeMethod(); + } } } @@ -549,9 +575,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) { @@ -575,18 +603,18 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { orgId = await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1]); } - this.platformUtilsService.showToast( - "success", - this.i18nService.t("organizationCreated"), - this.i18nService.t("organizationReadyToGo"), - ); + this.toastService.showToast({ + variant: "success", + title: this.i18nService.t("organizationCreated"), + message: this.i18nService.t("organizationReadyToGo"), + }); } else { orgId = await this.updateOrganization(orgId); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("organizationUpgraded"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("organizationUpgraded"), + }); } await this.apiService.refreshIdentityToken(); @@ -630,10 +658,18 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.buildSecretsManagerRequest(request); if (this.upgradeRequiresPaymentMethod) { - const tokenResult = await this.paymentComponent.createPaymentToken(); + let type: PaymentMethodType; + let token: string; + + if (this.deprecateStripeSourcesAPI) { + ({ type, token } = await this.paymentV2Component.tokenize()); + } else { + [token, type] = await this.paymentComponent.createPaymentToken(); + } + const paymentRequest = new PaymentRequest(); - paymentRequest.paymentToken = tokenResult[0]; - paymentRequest.paymentMethodType = tokenResult[1]; + paymentRequest.paymentToken = token; + paymentRequest.paymentMethodType = type; paymentRequest.country = this.taxComponent.taxFormGroup?.value.country; paymentRequest.postalCode = this.taxComponent.taxFormGroup?.value.postalCode; await this.organizationApiService.updatePayment(this.organizationId, paymentRequest); @@ -670,10 +706,17 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { if (this.selectedPlan.type === PlanType.Free) { request.planType = PlanType.Free; } else { - const tokenResult = await this.paymentComponent.createPaymentToken(); + let type: PaymentMethodType; + let token: string; - request.paymentToken = tokenResult[0]; - request.paymentMethodType = tokenResult[1]; + if (this.deprecateStripeSourcesAPI) { + ({ type, token } = await this.paymentV2Component.tokenize()); + } else { + [token, type] = await this.paymentComponent.createPaymentToken(); + } + + request.paymentToken = token; + request.paymentMethodType = type; request.additionalSeats = this.formGroup.controls.additionalSeats.value; request.additionalStorageGb = this.formGroup.controls.additionalStorage.value; request.premiumAccessAddon = diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index e11cf602ad2..341324c4a2a 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -1,9 +1,9 @@ - + - {{ "loading" | i18n }} + {{ "loading" | i18n }} {{ "subscriptionExpiration" | i18n }} -
    +
    {{ nextInvoice ? (nextInvoice.date | date: "mediumDate") : "-" }}
    +
    + {{ nextInvoice ? (sub.subscription.periodEndDate | date: "mediumDate") : "-" }} +
    @@ -66,16 +69,27 @@ >
    - {{ - "details" | i18n - }} + {{ "details" | i18n + }}{{ "providerDiscount" | i18n: customerDiscount?.percentOff }}
    - @@ -109,7 +135,7 @@ -
    +
    + + + +

    {{ "paymentMethod" | i18n }}

    +

    {{ "noPaymentMethod" | i18n }}

    + + + +

    + + {{ paymentSource.description }} + - {{ "unverified" | i18n }} +

    +
    + +

    + {{ "paymentChargedWithUnpaidSubscription" | i18n }} +

    +
    + + +

    {{ "taxInformation" | i18n }}

    +

    {{ "taxInformationDesc" | i18n }}

    + + +
    + + diff --git a/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts new file mode 100644 index 00000000000..0756a6c314c --- /dev/null +++ b/apps/web/src/app/billing/organizations/payment-method/organization-payment-method.component.ts @@ -0,0 +1,169 @@ +import { Component, ViewChild } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ActivatedRoute, Router } from "@angular/router"; +import { from, lastValueFrom, switchMap } from "rxjs"; + +import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; +import { PaymentMethodType } from "@bitwarden/common/billing/enums"; +import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; +import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; +import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { DialogService, ToastService } from "@bitwarden/components"; + +import { TaxInfoComponent } from "../../shared"; +import { + AddCreditDialogResult, + openAddCreditDialog, +} from "../../shared/add-credit-dialog.component"; +import { + AdjustPaymentDialogV2Component, + AdjustPaymentDialogV2ResultType, +} from "../../shared/adjust-payment-dialog/adjust-payment-dialog-v2.component"; + +@Component({ + templateUrl: "./organization-payment-method.component.html", +}) +export class OrganizationPaymentMethodComponent { + @ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent; + + organizationId: string; + accountCredit: number; + paymentSource?: PaymentSourceResponse; + subscriptionStatus?: string; + + loading = true; + + protected readonly Math = Math; + + constructor( + private activatedRoute: ActivatedRoute, + private billingApiService: BillingApiServiceAbstraction, + private dialogService: DialogService, + private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, + private router: Router, + private toastService: ToastService, + ) { + this.activatedRoute.params + .pipe( + takeUntilDestroyed(), + switchMap(({ organizationId }) => { + if (this.platformUtilsService.isSelfHost()) { + return from(this.router.navigate(["/settings/subscription"])); + } + + this.organizationId = organizationId; + return from(this.load()); + }), + ) + .subscribe(); + } + + protected addAccountCredit = async (): Promise => { + const dialogRef = openAddCreditDialog(this.dialogService, { + data: { + organizationId: this.organizationId, + }, + }); + + const result = await lastValueFrom(dialogRef.closed); + + if (result === AddCreditDialogResult.Added) { + await this.load(); + } + }; + + protected load = async (): Promise => { + this.loading = true; + const { accountCredit, paymentSource, subscriptionStatus } = + await this.billingApiService.getOrganizationPaymentMethod(this.organizationId); + this.accountCredit = accountCredit; + this.paymentSource = paymentSource; + this.subscriptionStatus = subscriptionStatus; + this.loading = false; + }; + + protected updatePaymentMethod = async (): Promise => { + const dialogRef = AdjustPaymentDialogV2Component.open(this.dialogService, { + data: { + initialPaymentMethod: this.paymentSource?.type, + organizationId: this.organizationId, + }, + }); + + const result = await lastValueFrom(dialogRef.closed); + + if (result === AdjustPaymentDialogV2ResultType.Submitted) { + await this.load(); + } + }; + + protected updateTaxInformation = async (): Promise => { + this.taxInfoComponent.taxFormGroup.updateValueAndValidity(); + this.taxInfoComponent.taxFormGroup.markAllAsTouched(); + + if (this.taxInfoComponent.taxFormGroup.invalid) { + return; + } + + const request = new ExpandedTaxInfoUpdateRequest(); + request.country = this.taxInfoComponent.country; + request.postalCode = this.taxInfoComponent.postalCode; + request.taxId = this.taxInfoComponent.taxId; + request.line1 = this.taxInfoComponent.line1; + request.line2 = this.taxInfoComponent.line2; + request.city = this.taxInfoComponent.city; + request.state = this.taxInfoComponent.state; + + await this.billingApiService.updateOrganizationTaxInformation(this.organizationId, request); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("taxInfoUpdated"), + }); + }; + + protected verifyBankAccount = async (request: VerifyBankAccountRequest): Promise => { + await this.billingApiService.verifyOrganizationBankAccount(this.organizationId, request); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("verifiedBankAccount"), + }); + }; + + protected get accountCreditHeaderText(): string { + const key = this.accountCredit <= 0 ? "accountBalance" : "accountCredit"; + return this.i18nService.t(key); + } + + protected get paymentSourceClasses() { + if (this.paymentSource == null) { + return []; + } + switch (this.paymentSource.type) { + case PaymentMethodType.Card: + return ["bwi-credit-card"]; + case PaymentMethodType.BankAccount: + return ["bwi-bank"]; + case PaymentMethodType.Check: + return ["bwi-money"]; + case PaymentMethodType.PayPal: + return ["bwi-paypal text-primary"]; + default: + return []; + } + } + + protected get subscriptionIsUnpaid(): boolean { + return this.subscriptionStatus === "unpaid"; + } + + protected get updatePaymentSourceButtonText(): string { + const key = this.paymentSource == null ? "addPaymentMethod" : "changePaymentMethod"; + return this.i18nService.t(key); + } +} diff --git a/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts b/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts index 50abcc92ba7..bc8694a5058 100644 --- a/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts +++ b/apps/web/src/app/billing/organizations/sm-adjust-subscription.component.ts @@ -6,6 +6,7 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { OrganizationSmSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-sm-subscription-update.request"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; export interface SecretsManagerSubscriptionOptions { interval: "year" | "month"; @@ -100,6 +101,7 @@ export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDest private organizationApiService: OrganizationApiServiceAbstraction, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, + private toastService: ToastService, ) {} ngOnInit() { @@ -158,11 +160,11 @@ export class SecretsManagerAdjustSubscriptionComponent implements OnInit, OnDest request, ); - await this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("subscriptionUpdated"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("subscriptionUpdated"), + }); this.onAdjusted.emit(); }; diff --git a/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts b/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts index 1f8b70e03fe..aae799d8089 100644 --- a/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts +++ b/apps/web/src/app/billing/organizations/sm-subscribe-standalone.component.ts @@ -11,6 +11,7 @@ import { BillingCustomerDiscount } from "@bitwarden/common/billing/models/respon import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ToastService } from "@bitwarden/components"; import { secretsManagerSubscribeFormFactory } from "../shared"; @@ -33,6 +34,7 @@ export class SecretsManagerSubscribeStandaloneComponent { private i18nService: I18nService, private organizationApiService: OrganizationApiServiceAbstraction, private organizationService: InternalOrganizationServiceAbstraction, + private toastService: ToastService, ) {} submit = async () => { @@ -60,11 +62,11 @@ export class SecretsManagerSubscribeStandaloneComponent { */ await this.apiService.refreshIdentityToken(); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("subscribedToSecretsManager"), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("subscribedToSecretsManager"), + }); this.onSubscribe.emit(); }; diff --git a/apps/web/src/app/billing/services/billing-services.module.ts b/apps/web/src/app/billing/services/billing-services.module.ts new file mode 100644 index 00000000000..7412d47c79c --- /dev/null +++ b/apps/web/src/app/billing/services/billing-services.module.ts @@ -0,0 +1,4 @@ +import { NgModule } from "@angular/core"; + +@NgModule({}) +export class BillingServicesModule {} diff --git a/libs/common/src/billing/services/payment-processors/braintree.service.ts b/apps/web/src/app/billing/services/braintree.service.ts similarity index 64% rename from libs/common/src/billing/services/payment-processors/braintree.service.ts rename to apps/web/src/app/billing/services/braintree.service.ts index 98533a54344..04b2b7dd442 100644 --- a/libs/common/src/billing/services/payment-processors/braintree.service.ts +++ b/apps/web/src/app/billing/services/braintree.service.ts @@ -1,12 +1,19 @@ -import { LogService } from "../../../platform/abstractions/log.service"; -import { BraintreeServiceAbstraction } from "../../abstractions"; +import { Injectable } from "@angular/core"; -export class BraintreeService implements BraintreeServiceAbstraction { +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; + +import { BillingServicesModule } from "./billing-services.module"; + +@Injectable({ providedIn: BillingServicesModule }) +export class BraintreeService { private braintree: any; private containerId: string; constructor(private logService: LogService) {} + /** + * Utilizes the Braintree SDK to create a [Braintree drop-in]{@link https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html} instance attached to the container ID specified as part of the {@link loadBraintree} method. + */ createDropin() { window.setTimeout(() => { const window$ = window as any; @@ -37,6 +44,12 @@ export class BraintreeService implements BraintreeServiceAbstraction { }, 250); } + /** + * Loads the Bitwarden dropin.js script in the element of the current page. + * This script attaches the Braintree SDK to the window. + * @param containerId - The ID of the HTML element where the Braintree drop-in will be loaded at. + * @param autoCreateDropin - Specifies whether the Braintree drop-in should be created when dropin.js loads. + */ loadBraintree(containerId: string, autoCreateDropin: boolean) { const script = window.document.createElement("script"); script.id = "dropin-script"; @@ -49,6 +62,10 @@ export class BraintreeService implements BraintreeServiceAbstraction { window.document.head.appendChild(script); } + /** + * Invokes the Braintree [requestPaymentMethod]{@link https://braintree.github.io/braintree-web-drop-in/docs/current/Dropin.html#requestPaymentMethod} method + * in order to generate a payment method token using the active Braintree drop-in. + */ requestPaymentMethod(): Promise { return new Promise((resolve, reject) => { this.braintree.requestPaymentMethod((error: any, payload: any) => { @@ -62,6 +79,12 @@ export class BraintreeService implements BraintreeServiceAbstraction { }); } + /** + * Removes the following elements from the of the current page: + * - The Bitwarden dropin.js script + * - Any
    - {{ i.name }} {{ i.quantity > 1 ? "×" + i.quantity : "" }} @ + {{ i.name }} {{ i.quantity > 1 ? "×" + i.quantity : "" }} @ {{ i.amount | currency: "$" }} {{ i.quantity * i.amount | currency: "$" }} /{{ i.interval | i18n }}
    + {{ i.productName | i18n }} - - {{ i.name }} {{ i.quantity > 1 ? "×" + i.quantity : "" }} @ + {{ i.name }} {{ i.quantity > 1 ? "×" + i.quantity : "" }} @ {{ i.amount | currency: "$" }} @@ -88,7 +102,19 @@ {{ "freeForOneYear" | i18n }} - {{ i.quantity * i.amount | currency: "$" }} /{{ i.interval | i18n }} +
    + + {{ i.quantity * i.amount | currency: "$" }} /{{ i.interval | i18n }} + + {{ + calculateTotalAppliedDiscount(i.quantity * i.amount) | currency: "$" + }} + / {{ "year" | i18n }} +